JavaRush /בלוג Java /Random-HE /סיפורו של ראיון אחד: שאלות מעניינות
GuitarFactor
רָמָה
Санкт-Петербург

סיפורו של ראיון אחד: שאלות מעניינות

פורסם בקבוצה
לאחרונה הזדמן לי להגיע לראיון לתפקיד מתמחה באחת מחברות ה-IT הגדולות. סיפורו של ראיון אחד: שאלות מעניינות - 1זה היה ראיון ה-IT הראשון שלי, ולדעתי זה התברר כמעניין. בסך הכל "נחקרתי" יותר מ-3 שעות (קדמו לכך שיעורי בית ומבחן במשרד במחשב). אני רוצה לחלוק כבוד למראיין, שלא ויתר כשעניתי על השאלה בצורה לא נכונה, אבל בעזרת השאלות המובילות שלו אילץ אותי לחשוב ולהגיע לתשובה הנכונה. להלן אציג מספר "סקיצות" - לדעתי, שאלות מעניינות למדי, שחלקן נתנו לי הבנה מעמיקה יותר של היבטים מסוימים בג'אווה. אולי הדברים האלה ייראו ברורים לחלק, אבל אני חושב שיהיו כאלה שזה יהיה שימושי עבורם. להלן הביטויים מודגשים בפונטים הבאים: מראיין - בהסברים מודגשים ב- Voice-over והמחשבות שלי - באותיות נטוי התשובות שלי - בפונט רגיל סיימנו עם הרקע, בוא ניגש לעניינים)

סקיצה 1. "שיטה פשוטה לכאורה"

כתבו כיצד הייתם מיישמים שיטה שמחזירה את התוצאה של חלוקת מספר א' במספר ב. המראיין כותב על פיסת נייר
int divide(int a, int b) {
}
*הצצתי בהתלהבות על פיסת הנייר עם חתימת השיטה. מה הקאץ'?* אני כותב:
int divide(int a, int b) {
    return a/b;
}
האם יש בעיות בשיטה זו? *אני תופס טיפש ממש טיפש* כנראה שלא.. בהמשך מגיעה שאלה לגיטימית: מה אם b=0? *וואו, אני עומד להעיף מהמשרד הזה אם אמשיך ככה!* אה כן, כמובן. כאן יש לנו טיעונים מסוג int, אז נזרק חריגה אריתמטית. אם הארגומנטים היו מסוג float או double, התוצאה תהיה אינסוף. מה אנחנו הולכים לעשות בנידון? אני מתחיל לכתוב נסה/תפוס
int divide(int a, int b) {
    try {
        return a/b;
    } catch (Exception e) {
        e.printStackTrace();
        return ... // ??? what the hack?
    }
}
*אני יכול להחזיר ולהקפיא: צריך להחזיר משהו במקרה של שגיאה. אך כיצד ניתן להבדיל בין ה"משהו" הזה לבין תוצאת החישוב?* מה נחזיר? הממ... הייתי משנה את סוג משתנה ההחזר ל-Integer ובמקרה של חריגה הייתי מחזיר null. בואו נדמיין שאנחנו לא יכולים לשנות את הסוג. אפשר איכשהו לצאת? אולי נוכל לעשות משהו אחר עם היוצא מן הכלל? *הנה זה בא* אנחנו יכולים גם להעביר את זה לשיטת הקריאה! ימין. איך זה ייראה?
int divide(int a, int b) throws ArithmeticException{
    return a/b;
}

void callDivide(int a, int b) {
    try {
        divide(a, b);
    } catch (ArithmeticException e) {
        e.printStackTrace();
    }
}
האם יש צורך לטפל בחריג? כן, כי אנחנו מעבירים את זה במפורש משיטת החלוקה. (*טעיתי כאן! להלן שאלות מובילות של המראיין להגיע לתשובה הנכונה*) וחריגה אריתמטית - באיזה חריג מדובר - מסומנת או לא מסומנת? זהו חריג זמן ריצה, כלומר לא מסומן. *הנה באה שאלת הרוצח* אז מסתבר, במילים שלך, אם ציינו זורק אריתמטיקה חריגה בחתימת השיטה, אז זה הפך לחריג מסומן? *איכס!* כנראה... לא. כן, זה נעלם. אם נציין זריקות / חריגה לא מסומנת/ בחתימה, אנו רק מתריעים שהמתודה יכולה לזרוק חריגה, אך אין צורך לטפל בה בשיטת הקריאה. זה סודר. האם יש עוד משהו שאנחנו יכולים לעשות כדי למנוע טעויות? *לאחר מחשבה* כן, אנחנו יכולים גם לבדוק אם (b==0). ותבצע קצת היגיון. ימין. אז אנחנו יכולים ללכת ב-3 דרכים:
  • נסה לתפוס
  • throws - העברה לשיטת הקריאה
  • בדיקת טיעונים
במקרה זה, divideאיזו שיטה עדיפה לדעתך?
הייתי בוחר להעביר את החריגה לשיטת ההתקשרות, כי... בשיטת החלוקה לא ברור כיצד לעבד את החריג הזה ואיזה סוג תוצאה intלהחזיר במקרה של שגיאה. ובשיטת הקריאה, הייתי משתמש בארגומנט b כדי לבדוק אם הוא שווה לאפס. נראה שהתשובה הזו סיפקה את המרואיין, אבל למען האמת, אני לא בטוח שהתשובה הזו היא חד משמעית))

סקיצה 2. "מי מהיר יותר?"

אחרי השאלה הסטנדרטית, במה שונה ArrayList מ-LinkedList, הגיע זה: מה יקרה מהר יותר - הכנסת אלמנט לאמצע ArrayListאו לאמצע LinkedList? *כאן קפצתי, נזכרתי שבכל מקום קראתי משהו כמו "השתמש LinkedListכדי להכניס או להסיר אלמנטים באמצע הרשימה." בבית אפילו בדקתי פעמיים את ההרצאות של JavaRush, יש משפט: "אם אתה הולך להכניס (או למחוק) אלמנטים רבים באמצע אוסף, אז כדאי שתשתמש ב- LinkedList. בכל שאר המקרים - ArrayList." מענה אוטומטי* זה יהיה מהיר יותר עם LinkedList. תבהיר בבקשה
  1. על מנת להכניס אלמנט באמצע ArrayList, נמצא את האלמנט ברשימה בזמן קבוע, ולאחר מכן מחשבים מחדש את המדדים של האלמנטים מימין לזה שהוכנס, בזמן ליניארי.
  2. עבור LinkedList.. אנו מגיעים תחילה לאמצע בזמן ליניארי ולאחר מכן מכניסים אלמנט בזמן קבוע, משנים קישורים לאלמנטים שכנים.
אז מסתבר, מה יותר מהיר? הממ... מסתבר אותו הדבר. אבל מתי זה LinkedListיותר מהיר? מסתבר שכאשר אנו מכניסים אותו לחצי הראשון של הרשימה. לדוגמה, אם תכניס אותו ממש בהתחלה, תצטרך ArrayListלחשב מחדש את כל המדדים עד לזנב מאוד, אבל תצטרך LinkedListלשנות רק את ההתייחסות של האלמנט הראשון. מוסרי: אל תאמין ממש לכל מה שכתוב, אפילו ב-JavaRush!)

סקיצה 3. "איפה היינו בלי שווים וקוד hashcode!"

השיחה על equals ו-hashcode הייתה מאוד ארוכה - איך לעקוף אותו, באיזה יישום יש Object, מה קורה מתחת למכסה המנוע, כשמכניסים אלמנט לתוך HashMapוכו'. אני רק אתן מספר נקודות שמעניינות לדעתי* תארו לעצמכם שיצרנו כיתה
public class A {
    int id;

    public A(int id) {
        this.id = id;
    }
}
והם לא דרסו equalsו hashcode. תאר מה יקרה כשהקוד יבוצע
A a1 = new A(1);
A a2 = new A(1);
Map<A, String> hash = new HashMap<>();
hash.put(a1, "1");
hash.get(a2);
*טוב שלפני הראיון ביליתי ספציפית כמה ימים בהבנת האלגוריתמים הבסיסיים, המורכבות שלהם ומבני הנתונים - זה עזר מאוד, תודה CS50!*
  1. צור שני מופעים של מחלקה A

  2. אנו יוצרים מפה ריקה, שבה כברירת מחדל יש 16 סלים. המפתח הוא אובייקט של מחלקה A, שבו מתודות equalsו אינן מבוטלות hashcode.

  3. שים את זה a1במפה. לשם כך, ראשית מחשבים את ה-hash a1.

    למה יהיה שווה ה-hash?

    הכתובת של תא בזיכרון היא יישום של מתודה ממחלקהObject

  4. על סמך ה-hash, אנו מחשבים את מדד הסל.

    איך נוכל לחשב את זה?

    *לצערי לא נתתי כאן תשובה ברורה. יש לך מספר ארוך - גיבוב, ויש 16 דליים - איך מגדירים אינדקס כך שאובייקטים עם גיבוב שונים יתפזרו באופן שווה על פני הדליים? יכולתי לדמיין שהמדד מחושב כך:

    int index = hash % buckets.length

    כבר בבית ראיתי שהיישום המקורי בקוד המקור שונה במקצת:

    static int indexFor(int h, int length)
    {
        return h & (length - 1);
    }
  5. אנחנו בודקים שאין התנגשויות ומכניסים a1.

  6. נעבור לשיטה get. מובטח למופעים a1 ו-a2 יש כתובת שונה hash(כתובת שונה בזיכרון), ולכן לא נמצא שום דבר עבור מפתח זה

    מה אם נגדיר את זה מחדש רק hashcodeבמחלקה A וננסה להכניס ל-hashmap תחילה זוג עם מפתח a1, ולאחר מכן עם a2?

    אז תחילה נמצא את הסל הרצוי על ידי hashcode- פעולה זו תתבצע בצורה נכונה. לאחר מכן, נתחיל לעבור על האובייקטים Entryב-LinkedList המצורפת לעגלה ונשווה את המפתחות לפי equals. כי equalsלא מוחלף, אז יישום הבסיס נלקח מהמחלקה Object- השוואה לפי הפניה. מובטח ל-a1 ו-a2 קישורים שונים, כך ש"נפספס" את האלמנט a1 שהוכנס, ו-a2 ימוקם ב-LinkedList כצומת חדש.

    מה המסקנה? האם זה אפשרי להשתמש כמפתח HashMapבאובייקט עם לא נדחק equalshashcode?

    לא אתה לא יכול.

סקיצה 4. "בואו נשבור את זה בכוונה!"

לאחר השאלות על שגיאה וחריגים, עלתה השאלה הבאה: כתוב דוגמה פשוטה שבה פונקציה תזרוק StackOverflow. *ואז נזכרתי איך השגיאה הזו הטרידה אותי כשניסיתי לכתוב איזו פונקציה רקורסיבית* זה כנראה יקרה במקרה של קריאה רקורסיבית, אם התנאי ליציאה מהרקורסיה יצוין בצורה שגויה. *ואז התחלתי להתחכם, בסוף המראיין עזר, הכל יצא פשוט*
void sof() {
    sof();
}
במה השגיאה הזו שונה מ- OutOfMemory? *לא עניתי כאן, רק מאוחר יותר הבנתי שזו שאלה על ידע Stackבזיכרון HeapJava (קריאות והפניות לאובייקטים מאוחסנים ב-Stack, והאובייקטים עצמם מאוחסנים בזיכרון ה-Heap). בהתאם לכך, StackOverflow נזרק כאשר אין יותר מקום בזיכרון Stackלקריאה הבאה לשיטה, והשטח OutOfMemoryלאובייקטים אזל בזיכרון Heap*
אלו הרגעים מהראיון שאני זוכר. בסופו של דבר התקבלתי להתמחות אז לפניי 2.5 חודשי הכשרה ואם הכל ילך כשורה עבודה בחברה) אם יש עניין אוכל לכתוב מאמר נוסף, הפעם קטן יותר, עם ניתוח של בעיה פשוטה אך ממחישה שקיבלתי ראיון בחברה אחרת. זה הכל בשבילי, אני מקווה שמאמר זה יעזור למישהו להעמיק או לארגן את הידע שלו. למידה שמחה לכולם!
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION