פרק 15 - תכנות מונחה עצמים מתקדם
בפרק 9 יצרנו מחלקות בודדות. עכשיו מרחיבים: מחלקה יכולה לרשת ממחלקה אחרת ולקבל ממנה שדות ומתודות "בחינם", אפשר לשתף מידע בין כל העצמים של מחלקה, ואפשר לבדוק בזמן ריצה מה הטיפוס האמיתי של עצם. זה בדיוק התוכן של מסלול ה-OOP בחלק ב' בשאלון 899271.
1. ירושה - extends ו-super
מחלקה יכולה להרחיב (extends) מחלקה אחרת ולקבל את כל השדות והמתודות שלה, ולהוסיף
משלה. נתחיל מהחלק הבסיסי ביותר - שדות ובנאי בלבד, בלי שום דבר מתקדם עדיין:
public class Ticket {
protected double price;
protected int seat;
public Ticket(double price, int seat) {
this.price = price;
this.seat = seat;
}
}
protected (במקום private) על price ו-seat
מאפשר למחלקות שיורשות מ-Ticket לגשת אליהם ישירות.
הבנאי של המחלקה היורשת חייב לקרוא לבנאי של האב באמצעות super(...) - וזו חייבת
להיות השורה הראשונה בבנאי:
public class VipTicket extends Ticket {
private int perks;
public VipTicket(double price, int seat, int perks) {
super(price, seat); // קודם קוראים לבנאי של Ticket
this.perks = perks;
}
}
2. שדה static - משותף לכל האובייקטים
שדה static לא שייך לאובייקט בודד - יש ממנו עותק אחד בלבד, משותף לכל
האובייקטים של המחלקה. נוסיף ל-Ticket מונה שסופר כמה כרטיסים נוצרו בסך הכול:
private static int totalTickets = 0; // משותף לכל האובייקטים - לא לכל אחד עותק משלו
public Ticket(double price, int seat) {
this.price = price;
this.seat = seat;
totalTickets++; // כל בנאי מגדיל את אותו מונה משותף
}
public static int getTotalTickets() {
return totalTickets;
}
static על totalTickets אומר: יש עותק אחד בלבד של
המשתנה הזה, משותף לכל העצמים מהמחלקה - כל קריאה לבנאי (גם דרך VipTicket, שקוראת
ל-super(...)) מגדילה את אותו המונה, בלי קשר לכמה אובייקטים ספציפיים קיימים.
מה יודפס למסך על ידי הקוד הבא?
Ticket a = new Ticket(10, 1);
Ticket b = new Ticket(20, 2);
Ticket c = new Ticket(30, 3);
System.out.println(Ticket.getTotalTickets());
3. דריסת מתודות (Override) עם super
מחלקה יורשת יכולה לדרוס מתודה של האב - לתת לה מימוש שונה. שם המתודה, סוג
ההחזרה והפרמטרים חייבים להיות זהים בדיוק, ומקובל לסמן @Override מעל. ל-Ticket
נוסיף מתודה שמחשבת את המחיר הסופי:
// ב-Ticket:
public double totalCost() {
return price;
}
// ב-VipTicket, דורסת את הגרסה של Ticket:
@Override
public double totalCost() {
return super.totalCost() + 50; // תוספת VIP, מעל המחיר הרגיל
}
super.totalCost() קוראת לגרסה של האב - דריסה לא חייבת להתעלם לגמרי מהמימוש
המקורי, אפשר "להוסיף עליו" במקום לשכתב אותו מאפס.
4. instanceof וקאסטינג
לפעמים מתודה דרוסה צריכה לגשת לשדה שקיים רק במחלקה היורשת, ולא במחלקת האב. לשם
כך קודם בודקים instanceof, ורק אם התוצאה true מבצעים קאסטינג:
// ב-Ticket:
public void combine(Ticket other) {
this.price += other.price;
System.out.println("Combined price: " + this.price);
}
// ב-VipTicket, דורסת ומוסיפה טיפול ב-perks:
@Override
public void combine(Ticket other) {
super.combine(other); // קודם מריצים את ההתנהגות הרגילה
if (other instanceof VipTicket) {
this.perks += ((VipTicket) other).perks;
System.out.println("Combined perks: " + this.perks);
}
}
instanceof בודק אם העצם other הוא בעצם (גם) מטיפוס
VipTicket - ורק אם כן, מותר לבצע קאסטינג ((VipTicket) other)
כדי לגשת לשדה perks שקיים רק ב-VipTicket ולא ב-Ticket.
5. טיפוס ההפניה מול טיפוס האובייקט בפועל
זו הנקודה שהכי הרבה תלמידים מתבלבלים בה: Ticket t2 = new VipTicket(80, 5, 3); - איך
זה אפשרי בכלל? t2 מוצהר (declared) כ-Ticket - זה
טיפוס ההפניה, ומגביל אילו שדות ומתודות אפשר לכתוב על t2 ישירות בקוד.
אבל האובייקט שבאמת נוצר בזיכרון עם new, ושה-t2 מצביע עליו, הוא
VipTicket - זה הטיפוס האמיתי (בפועל) של האובייקט.
הכלל: מתודה דרוסה (@Override) תמיד רצה לפי הטיפוס האמיתי של
האובייקט, לא לפי טיפוס ההפניה. לעומת זאת, אילו שדות/מתודות בכלל מותר לקרוא
להן על המשתנה בלי קאסטינג נקבע לפי טיפוס ההפניה - למשל t2.perks לא יתקמפל
בכלל, למרות שהאובייקט בפועל הוא VipTicket, כי המשתנה t2 "יודע" להיות רק
Ticket אלא אם עושים קאסטינג מפורש.
נתון: Ticket t = new VipTicket(10, 1, 2);. כשקוראים ל-t.totalCost(),
איזו גרסה של המתודה באמת רצה - זו של Ticket או זו של VipTicket?
6. הכל ביחד - המחלקות המלאות
עכשיו שהכרנו כל חלק בנפרד, הנה שתי המחלקות המלאות יחד - בדיוק כפי שהן עשויות להופיע בשאלון:
public class Ticket {
protected double price;
protected int seat;
private static int totalTickets = 0; // משותף לכל האובייקטים - לא לכל אחד עותק משלו
public Ticket(double price) {
this.price = price;
this.seat = 0;
totalTickets++;
}
public Ticket(double price, int seat) {
this.price = price;
this.seat = seat;
totalTickets++;
}
public static int getTotalTickets() {
return totalTickets;
}
public double totalCost() {
return price;
}
public void combine(Ticket other) {
this.price += other.price;
System.out.println("Combined price: " + this.price);
}
}
public class VipTicket extends Ticket {
private int perks;
public VipTicket(double price, int seat, int perks) {
super(price, seat); // קודם קוראים לבנאי של Ticket
this.perks = perks;
}
@Override
public double totalCost() {
return super.totalCost() + 50; // תוספת VIP, מעל המחיר הרגיל
}
@Override
public void combine(Ticket other) {
super.combine(other); // קודם מריצים את ההתנהגות הרגילה
if (other instanceof VipTicket) {
this.perks += ((VipTicket) other).perks;
System.out.println("Combined perks: " + this.perks);
}
}
}
7. דוגמה מלאה - מעקב אחרי הרצה
שאלות מהסוג הזה בבגרות נותנות כמה שורות קוד ומבקשות לחזות מה כל שורה מדפיסה - בואו נעקוב אחרי הרצה מלאה:
Ticket t1 = new Ticket(100);
Ticket t2 = new VipTicket(80, 5, 3); // הפניה מטיפוס Ticket, אובייקט בפועל VipTicket
VipTicket v1 = new VipTicket(120, 12, 2);
System.out.println(Ticket.getTotalTickets()); // (1)
System.out.println(t2.totalCost()); // (2)
t2.combine(v1); // (3)
(1) מדפיס 3 - getTotalTickets היא מתודת static
ששייכת למחלקה כולה, וכל שלוש הקריאות לבנאי (גם דרך VipTicket, שקוראת ל-
super(...)) הגדילו את אותו מונה משותף.
(2) מדפיס 130.0 - t2 מוצהר כ-Ticket, אבל
האובייקט בפועל הוא VipTicket. Java תמיד מריצה את הגרסה הדרוסה לפי הטיפוס
האמיתי של האובייקט, לא לפי טיפוס ההפניה - לכן רץ VipTicket.totalCost():
80 + 50 = 130.0.
(3) מדפיס "Combined price: 200.0" ואז "Combined perks: 5" - מאותה סיבה,
t2.combine(v1) מריצה את VipTicket.combine: קודם super.combine
מחבר את המחירים (80 + 120 = 200.0), ואז - מכיוון ש-v1 instanceof VipTicket
הוא true - מחבר גם את ה-perks (3 + 2 = 5).
8. טעויות נפוצות
קאסטינג בלי בדיקת instanceof קודם: (VipTicket) other כשה-
other בפועל הוא Ticket רגיל גורם ל-ClassCastException.
שכחת super() בבנאי: אם לא קוראים ל-super(...) במפורש כשלמחלקת
האב אין בנאי ריק, זו שגיאת קומפילציה.
בלבול בין טיפוס ההפניה לטיפוס האובייקט: משתנה מוצהר כ-Ticket
עדיין יכול "להחזיק" אובייקט VipTicket - ומתודות דרוסות תמיד רצות לפי האובייקט
בפועל, לא לפי סוג המשתנה.
🎓 זווית הבגרות
איך זה מופיע בבגרות: זה בדיוק תוכן חלק ב' של מסלול ה-OOP בשאלון 899271 - שאלה עם 2-3 מחלקות בירושה, ובקשה לחזות מה תדפיס סדרת שורות קוד.
סוג שאלה טיפוסי: מעקב אחרי הרצה (בדיוק כמו הדוגמה המלאה
למעלה) - פולימורפיזם, שדה static משותף, ו-instanceof + קאסטינג.
טעות נפוצה: לחשוב שמתודה דרוסה רצה לפי טיפוס המשתנה
(Ticket) ולא לפי הטיפוס האמיתי של האובייקט (VipTicket) - זו
הטעות מספר 1 בנושא.
מה לזכור למבחן: תמיד לשאול "מה האובייקט באמת בפועל?" ולא "איך המשתנה מוצהר?" - הטיפוס בפועל קובע איזו גרסה דרוסה תרוץ.
9. תרגול
מה יודפס למסך על ידי הקוד הבא?
Ticket t = new VipTicket(50, 1, 10);
System.out.println(t.totalCost());
כתבו את שורת הקריאה לבנאי של מחלקת האב, עם הפרמטרים price ו-seat,
כשורה הראשונה בבנאי של VipTicket.
כתבו את הביטוי שבודק אם המשתנה other מצביע בפועל על אובייקט מטיפוס
VipTicket.
כתבו את שורת הקוד בתוך totalCost() של VipTicket שקוראת לגרסה
של מחלקת האב ומוסיפה לה 50.