JavaRush /בלוג Java /Random-HE /ניתוח שאלות ותשובות מראיונות למפתח Java. חלק 14

ניתוח שאלות ותשובות מראיונות למפתח Java. חלק 14

פורסם בקבוצה
זִקוּקֵי דִי נוּר! העולם כל הזמן זז ואנחנו כל הזמן זזים. בעבר, כדי להפוך למפתח ג'אווה, היה מספיק לדעת קצת תחביר ג'אווה, והשאר יגיע. עם הזמן, רמת הידע הנדרשת כדי להפוך למפתח ג'אווה גדלה משמעותית, וכך גם התחרות, שממשיכה לדחוף את הרף התחתון של הידע הנדרש כלפי מעלה. ניתוח שאלות ותשובות מראיונות למפתח Java.  חלק 14 - 1אם אתה באמת רוצה להיות מפתח, אתה צריך לקחת את זה כמובן מאליו ולהתכונן ביסודיות כדי לבלוט בקרב מתחילים כמוך. מה נעשה היום, כלומר, נמשיך לנתח את 250+ השאלות . במאמרים קודמים בדקנו את כל השאלות ברמה הזוטר, והיום נטפל בשאלות ברמה הביניים. אמנם אני מציין שלא מדובר ב-100% בשאלות דרג ביניים, אבל את רובן תוכלו לפגוש בראיון לדרג חטיבת ביניים, כי דווקא בראיונות כאלה מתבצע בדיקה מפורטת של הבסיס התיאורטי שלכם, בעוד שלתלמיד חטיבת הביניים השאלות מתמקדות יותר בבדיקת הניסיון שלו. ניתוח שאלות ותשובות מראיונות למפתח Java.  חלק 14 - 2אבל, ללא עיכובים נוספים, בואו נתחיל.

אֶמצַע

נפוצים

1. מהם היתרונות והחסרונות של OOP בהשוואה לתכנות פרוצדורלי/פונקציונלי?

הייתה שאלה זו בניתוח השאלות לג'וניור, ובהתאם לכך כבר עניתי עליה. חפש את השאלה הזו ואת התשובה שלה בחלק זה של המאמר, שאלות 16 ו-17.

2. במה שונה צבירה מהרכב?

ב-OOP, ישנם מספר סוגים של אינטראקציה בין אובייקטים, המאוחדים תחת המושג הכללי של "יש יחסים". קשר זה מצביע על כך שאובייקט אחד הוא מרכיב של אובייקט אחר. יחד עם זאת, ישנם שני תתי סוגים של מערכת יחסים זו: קומפוזיציה - אובייקט אחד יוצר אובייקט אחר ואורך חייו של אובייקט אחר תלוי בחייו של היוצר. צבירה - אובייקט מקבל קישור (מצביע) לאובייקט אחר במהלך תהליך הבנייה (במקרה זה, משך החיים של האובייקט השני אינו תלוי בחייו של היוצר). להבנה טובה יותר, בואו נסתכל על דוגמה ספציפית. יש לנו מחלקה מסוימת של מכוניות - Car , שבתורה יש לה שדות פנימיים מהסוג - מנוע ורשימת נוסעים - List<Passenger> , יש לה גם שיטה להתחלת תנועה - startMoving() :
public class Car {

 private Engine engine;
 private List<Passenger> passengers;

 public Car(final List<Passenger> passengers) {
   this.engine = new Engine();
   this.passengers = passengers;
 }

 public void addPassenger(Passenger passenger) {
   passengers.add(passenger);
 }

 public void removePassengerByIndex(Long index) {
   passengers.remove(index);
 }

 public void startMoving() {
   engine.start();
   System.out.println("Машина начала своё движение");
   for (Passenger passenger : passengers) {
     System.out.println("В машине есть пассажир - " + passenger.getName());
   }
 }
}
במקרה זה, הקומפוזיציה היא החיבור בין מכונית למנוע , מכיוון שביצועי המכונית תלויים ישירות בנוכחות אובייקט המנוע, כי אם engine = null , אז נקבל NullPointerException . בתורו, מנוע לא יכול להתקיים בלי מכונה (למה אנחנו צריכים מנוע בלי מכונה?) ואינו יכול להיות שייך למספר מכונות בנקודת זמן אחת. משמעות הדבר היא שאם נמחק את אובייקט המכונית , לא יהיו יותר הפניות לאובייקט המנוע , והוא יימחק בקרוב על ידי אוסף הזבל . כפי שאתה יכול לראות, הקשר הזה מאוד קפדני (חזק). צבירה היא הקשר בין רכב לנוסע , שכן הביצועים של רכב אינם תלויים בשום אופן בחפצים מסוג הנוסע ובמספרם. הם יכולים לעזוב את המכונית - removePassengerByIndex(Long index) או להזין חדשים - addPassenger(Passenger passager) , למרות זאת, המכונית תמשיך לתפקד כראוי. בתורו, אובייקטים של נוסעים יכולים להתקיים ללא אובייקט מכונית . כפי שהבנתם, מדובר בחיבור הרבה יותר חלש ממה שאנו רואים בהרכב. אבל זה לא הכל, לאובייקט שמחובר בצבירה לאחר יכול להיות גם קשר נתון עם אובייקטים אחרים באותה נקודת זמן. לדוגמה, אתה, כסטודנט ג'אווה, רשום לקורסי אנגלית, OOP ולוגריתמים באותה נקודת זמן, אך יחד עם זאת אתה לא חלק הכרחי מהם, שבלעדיהם אי אפשר לתפקד רגיל (כגון מורה). ניתוח שאלות ותשובות מראיונות למפתח Java.  חלק 14 - 3

3. באילו דפוסי GoF השתמשת בפועל? תן דוגמאות.

כבר עניתי על השאלה הזו בעבר, אז אני פשוט אשאיר קישור לניתוח , ראה את השאלה הראשונה. מצאתי גם כתבה נפלאה על דפוסי עיצוב, שאני ממליצה לשמור בהישג יד.

4. מהו אובייקט פרוקסי? תן דוגמאות

פרוקסי הוא תבנית עיצוב מבנית המאפשרת לך להחליף אובייקטים תחליפיים מיוחדים, או במילים אחרות, אובייקטי פרוקסי, במקום אובייקטים אמיתיים. אובייקטי פרוקסי אלה מיירטים שיחות לאובייקט המקורי, ומאפשרים להכניס היגיון כלשהו לפני או אחרי שהקריאה מועברת למקור. ניתוח שאלות ותשובות מראיונות למפתח Java.  חלק 14 - 4דוגמאות לשימוש באובייקט פרוקסי:
  • כפרוקסי מרוחק - משמש כאשר אנו צריכים אובייקט מרוחק (אובייקט במרחב כתובות אחר) שצריך להיות מיוצג מקומי. במקרה זה, ה-proxy יטפל ביצירת חיבור, קידוד, פענוח וכו', בעוד שהלקוח ישתמש בו כאילו היה האובייקט המקורי שנמצא במרחב המקומי.

  • כפרוקסי וירטואלי - משמש כאשר יש צורך באובייקט עתיר משאבים. במקרה זה, אובייקט ה-proxy משמש כמשהו כמו תמונה של אובייקט אמיתי שבעצם עדיין לא קיים. כאשר נשלחת בקשה אמיתית (קריאה לשיטה) לאובייקט זה, רק אז האובייקט המקורי נטען והשיטה מתבצעת. גישה זו נקראת גם אתחול עצלן; זה יכול להיות מאוד נוח, מכיוון שבמצבים מסוימים ייתכן שהאובייקט המקורי לא יהיה שימושי, ואז לא תהיה עלות ליצור אותו.

  • כפרוקסי אבטחה - משמש כאשר אתה צריך לשלוט בגישה לאובייקט כלשהו בהתבסס על זכויות הלקוח. כלומר, אם לקוח עם זכויות גישה חסרות ינסה לגשת לאובייקט המקורי, ה-proxy יירט אותו ולא יאפשר זאת.

בואו נסתכל על דוגמה של פרוקסי וירטואלי: יש לנו ממשק מטפל כלשהו:
public interface Processor {
 void process();
}
היישום שלו משתמש ביותר מדי משאבים, אך יחד עם זאת לא ניתן להשתמש בו בכל פעם שהאפליקציה מופעלת:
public class HiperDifficultProcessor implements Processor {
 @Override
 public void process() {
   // некоторый сверхсложная обработка данных
 }
}
מחלקת פרוקסי:
public class HiperDifficultProcessorProxy implements Processor {
private HiperDifficultProcessor processor;

 @Override
 public void process() {
   if (processor == null) {
     processor = new HiperDifficultProcessor();
   }
   processor.process();
 }
}
בוא נריץ את זה ב- main :
Processor processor = new HiperDifficultProcessorProxy();
// тут тяжеловсеного оригинального an object, ещё не сущетсвует
// но при этом есть an object, который его представляет и у которого можно вызывать его методы
processor.process(); // лишь теперь, an object оригинал был создан
אני מציין שמסגרות רבות משתמשות ב-proxy, ועבור Spring זו תבנית מפתח (ספרינג נתפר איתו מבפנים ומבחוץ). קרא עוד על דפוס זה כאן . ניתוח שאלות ותשובות מראיונות למפתח Java.  חלק 14 - 5

5. אילו חידושים הוכרזו ב-Java 8?

החידושים שהביא Java 8 הם כדלקמן:
  • נוספו ממשקים פונקציונליים, קראו על איזו חיה זו כאן .

  • ביטויי למדה, הקשורים קשר הדוק לממשקים פונקציונליים, קרא עוד על השימוש בהם כאן .

  • נוסף Stream API לעיבוד נוח של אוספי נתונים, קרא עוד כאן .

  • נוספו קישורים לשיטות .

  • שיטת forEach() נוספה לממשק Iterable .

  • נוסף API חדש לתאריך ושעה בחבילת java.time , ניתוח מפורט כאן .

  • API משופר במקביל .

  • הוספת מחלקת עטיפה אופציונלית , המשמשת לטיפול נכון בערכי null, תוכל למצוא כאן מאמר מצוין בנושא זה .

  • הוספת היכולת של ממשקים להשתמש בשיטות סטטיות ובשיטות ברירת מחדל (מה שבעצם מקרב את Java לירושה מרובה), פרטים נוספים כאן .

  • נוספו שיטות חדשות למחלקה Collection(removeIf(), spliterator()) .

  • שיפורים קלים ל-Java Core.

ניתוח שאלות ותשובות מראיונות למפתח Java.  חלק 14 - 6

6. מה הם לכידות גבוהה וצימוד נמוך? תן דוגמאות.

High Cohesion או High Cohesion הוא המושג כאשר מחלקה מסוימת מכילה אלמנטים הקשורים זה לזה באופן הדוק ומשולבים למטרתם. לדוגמה, כל השיטות במחלקת User צריכות לייצג את התנהגות המשתמש. לכיתה יש לכידות נמוכה אם היא מכילה אלמנטים לא קשורים. לדוגמה, המחלקה User המכילה שיטת אימות כתובת דוא"ל:
public class User {
private String name;
private String email;

 public String getName() {
   return this.name;
 }

 public void setName(final String name) {
   this.name = name;
 }

 public String getEmail() {
   return this.email;
 }

 public void setEmail(final String email) {
   this.email = email;
 }

 public boolean isValidEmail() {
   // некоторая логика валидации емейла
 }
}
כיתת המשתמש עשויה להיות אחראית לשמירת כתובת האימייל של המשתמש, אך לא לאימותה או שליחת המייל. לכן, על מנת להשיג קוהרנטיות גבוהה, אנו מעבירים את שיטת האימות למחלקת שירות נפרדת:
public class EmailUtil {
 public static boolean isValidEmail(String email) {
   // некоторая логика валидации емейла
 }
}
ואנחנו משתמשים בו לפי הצורך (לדוגמה, לפני שמירת המשתמש). Low Coupling או Low Coupling הוא מושג המתאר תלות הדדית נמוכה בין מודולי תוכנה. בעיקרו של דבר, תלות הדדית היא האופן שבו שינוי אחד דורש שינוי של השני. לשתי מחלקות יש צימוד חזק (או צימוד הדוק) אם הם קשורים זה לזה. לדוגמה, שתי מחלקות קונקרטיות המאחסנות הפניות זו לזו וקוראות זו לשיטות של זו. קל יותר לפתח ולתחזק שיעורים בשילוב רופף. מכיוון שהם בלתי תלויים זה בזה, ניתן לפתח ולבדוק אותם במקביל. יתר על כן, ניתן לשנות ולעדכן אותם מבלי להשפיע זה על זה. בואו נסתכל על דוגמה של כיתות משולבות חזקות. יש לנו כמה כיתת סטודנטים:
public class Student {
 private Long id;
 private String name;
 private List<Lesson> lesson;
}
אשר מכיל רשימה של שיעורים:
public class Lesson {
 private Long id;
 private String name;
 private List<Student> students;
}
כל שיעור מכיל קישור להשתתפות תלמידים. אחיזה חזקה להפליא, אתה לא חושב? איך אפשר להפחית את זה? ראשית, בואו נוודא שלתלמידים אין רשימה של נושאים, אלא רשימה של המזהים שלהם:
public class Student {
 private Long id;
 private String name;
 private List<Long> lessonIds;
}
שנית, כיתת השיעור לא צריכה לדעת על כל התלמידים, אז בואו נמחק את הרשימה שלהם לגמרי:
public class Lesson {
 private Long id;
 private String name;
}
אז זה נעשה הרבה יותר קל, והחיבור הפך להיות הרבה יותר חלש, אתה לא חושב? ניתוח שאלות ותשובות מראיונות למפתח Java.  חלק 14 - 7

אוף

7. כיצד ניתן ליישם ירושה מרובה ב-Java?

ירושה מרובה היא תכונה של מושג מונחה עצמים שבו מחלקה יכולה לרשת מאפיינים מיותר ממחלקת אב אחת. הבעיה מתעוררת כאשר יש מתודות עם אותה חתימה גם במחלקת העל וגם בתת המחלקה. בעת קריאה למתודה, המהדר לא יכול לקבוע לאיזו שיטת מחלקה יש לקרוא, ואפילו בעת קריאה למתודת המחלקה שיש לה עדיפות. לכן, Java אינה תומכת בירושה מרובה! אבל יש מעין פרצה, עליה נדבר בהמשך. כפי שציינתי קודם לכן, עם שחרורו של Java 8, נוספה לממשקים היכולת לקבל שיטות ברירת מחדל . אם המחלקה המיישמת את הממשק אינה עוקפת את השיטה הזו, אזי ייעשה שימוש ביישום ברירת המחדל הזה (אין צורך לעקוף את שיטת ברירת המחדל, כמו הטמעת שיטת מופשטת). במקרה זה, ניתן ליישם ממשקים שונים במחלקה אחת ולהשתמש בשיטות ברירת המחדל שלהם. בואו נסתכל על דוגמה. יש לנו ממשק עלונים, עם שיטת ברירת המחדל של fly() :
public interface Flyer {
 default void fly() {
   System.out.println("Я лечу!!!");
 }
}
ממשק Walker, עם שיטת ברירת המחדל walk() :
public interface Walker {
 default void walk() {
   System.out.println("Я хожу!!!");
 }
}
ממשק השחיין, עם שיטת swim() :
public interface Swimmer {
 default void swim() {
   System.out.println("Я плыву!!!");
 }
}
ובכן, עכשיו בואו ליישם את כל זה בשיעור ברווז אחד:
public class Duck implements Flyer, Swimmer, Walker {
}
ובואו נריץ את כל השיטות של הברווז שלנו:
Duck donald = new Duck();
donald.walk();
donald.fly();
donald.swim();
בקונסולה נקבל:
אני הולך!!! אני עף!!! אני שוחה!!!
זה אומר שציירנו נכון ירושה מרובה, למרות שזה לא מה שזה. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 14 - 8אני גם מציין שאם מחלקה מיישמת ממשקים עם מתודות ברירת מחדל שיש להן אותם שמות של מתודות ואותם ארגומנטים בשיטות האלה, אז המהדר יתחיל להתלונן על חוסר תאימות, מכיוון שהוא לא מבין באיזו שיטה באמת צריך להשתמש. ישנן מספר דרכים לצאת:
  • שנה את שמות השיטות בממשקים כך שהן שונות זו מזו.
  • לעקוף שיטות שנויות במחלוקת כאלה במעמד היישום.
  • קבל בירושה מכיתה שמיישמת את השיטות השנויות במחלוקת הללו (ואז הכיתה שלך תשתמש בדיוק ביישום שלה).

8. מה ההבדל בין שיטות final, finally ו-finalize()?

final היא מילת מפתח המשמשת להצבת אילוץ על מחלקה, שיטה או משתנה, כלומר אילוץ:
  • עבור משתנה - לאחר אתחול ראשוני, לא ניתן להגדיר מחדש את המשתנה.
  • עבור מתודה, לא ניתן לעקוף את השיטה בתת-מחלקה (מחלקה יורשת).
  • עבור מחלקה - המחלקה לא יכולה לעבור בירושה.
finally היא מילת מפתח לפני בלוק קוד, המשמשת בעת טיפול בחריגים, בשילוב עם בלוק try , וביחד (או להחלפה) עם בלוק catch. הקוד בבלוק זה מבוצע בכל מקרה, ללא קשר אם נזרק חריג או לא. בחלק זה של המאמר, בשאלה 104, נדונים מצבים חריגים בהם חסימה זו לא תבוצע. finalize() היא שיטה של ​​המחלקה Object , הנקראת לפני שכל אובייקט נמחק על ידי אוסף האשפה, שיטה זו תיקרא (last), והיא משמשת לניקוי משאבים תפוסים. למידע נוסף על השיטות של מחלקת Object שכל אובייקט יורש, ראה שאלה 11 בחלק זה של המאמר. ובכן, שם נסיים היום. נתראה בחלק הבא! Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 14 - 9
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION