ברוכים הבאים לשיעור בנושא "ירושה" במסגרת הקורס "תכנות מתקדם בשפת Java" (20554). ירושה היא אחד מעמודי התווך של תכנות מונחה עצמים (OOP), ומאפשרת לנו לבנות היררכיות של מחלקות, להרחיב פונקציונליות קיימת ולעשות שימוש חוזר בקוד בצורה יעילה ומסודרת. הבנה מעמיקה של מנגנון הירושה חיונית לכתיבת קוד גמיש, קריא וקל לתחזוקה, והיא נושא מרכזי במבחני הקורס.
מנגנון הירושה: עקרונות יסוד
ירושה מאפשרת למחלקה אחת (מחלקה בן) לרשת תכונות (שדות) והתנהגויות (מתודות) ממחלקה אחרת (מחלקה אב). זהו יישום של עקרון ה- "IS-A" (הוא-א), כלומר, אובייקט של מחלקת בן "הוא סוג של" אובייקט של מחלקת אב.
ב-Java, אנו משתמשים במילת המפתח extends כדי להגדיר ירושה:
class Animal {
void eat() {
System.out.println("Animal eats.");
}
}
class Dog extends Animal { // Dog יורש מ-Animal
void bark() {
System.out.println("Dog barks.");
}
}
בדוגמה זו, Dog היא מחלקת הבן של Animal. אובייקט מסוג Dog יכלול גם את המתודה eat() בנוסף ל-bark().
מושגי מפתח בירושה
קריאה לבנאי אב (Calling Parent Constructor)
כאשר אנו יוצרים אובייקט של מחלקת בן, בנאי מחלקת הבן חייב, באופן מפורש או מרומז, לקרוא לבנאי של מחלקת האב. קריאה זו מבטיחה שכל החלקים של האובייקט (הן של האב והן של הבן) יאותחלו כראוי.
super(): מילת מפתח המשמשת לקריאה לבנאי של מחלקת האב מתוך בנאי מחלקת הבן. היא חייבת להיות השורה הראשונה בבנאי הבן.
אם לא נקרא לבנאי האב במפורש באמצעות super(...), המהדר של Java יוסיף אוטומטית קריאה ל-super() (הבנאי הריק של האב) כשורת הקוד הראשונה בבנאי הבן. אם למחלקת האב אין בנאי ריק, או אם אנו רוצים לקרוא לבנאי ספציפי של האב שמקבל פרמטרים, חובה עלינו לקרוא לו במפורש.
class Vehicle {
String brand;
Vehicle(String brand) {
this.brand = brand;
System.out.println("Vehicle constructor: " + brand);
}
}
class Car extends Vehicle {
int numberOfDoors;
Car(String brand, int doors) {
super(brand); // קריאה מפורשת לבנאי האב
this.numberOfDoors = doors;
System.out.println("Car constructor: " + brand + ", " + doors + " doors");
}
}
מתודות וירטואליות ו-Override
ב-Java, כל המתודות שאינן static או final הן "וירטואליות" באופן מרומז. המשמעות היא שניתן לדרוס (override) אותן במחלקות הבן. דריסה מאפשרת למחלקת הבן לספק יישום משלה למתודה שכבר קיימת במחלקת האב, תוך שמירה על אותה חתימה (שם, סוגי פרמטרים).
@Override: אנוטציה (annotation) אופציונלית אך מומלצת, המציינת שמתודה מסוימת דורסת מתודה ממחלקת אב. היא מסייעת למהדר לאתר שגיאות (למשל, אם חתימת המתודה אינה תואמת).כללים לדריסת מתודה:
- חתימת המתודה (שם, מספר וסוג הפרמטרים) חייבת להיות זהה.
- סוג ההחזרה (return type) חייב להיות זהה או covariant (סוג תת-מחלקה).
- רמת הגישה (access modifier) של המתודה הדורסת לא יכולה להיות מצמצמת יותר מזו של המתודה הנרמסת (למשל, מ-
publicל-private). - לא ניתן לדרוס מתודות
finalאוstatic.
class Animal {
void makeSound() {
System.out.println("Animal makes a sound.");
}
}
class Dog extends Animal {
@Override // מומלץ!
void makeSound() {
System.out.println("Dog barks!");
}
void fetch() {
System.out.println("Dog fetches.");
}
}
class Main {
public static void main(String[] args) {
Animal myAnimal = new Animal();
Animal myDogAsAnimal = new Dog(); // פולימורפיזם
Dog myDog = new Dog();
myAnimal.makeSound(); // Output: Animal makes a sound.
myDogAsAnimal.makeSound(); // Output: Dog barks! (קריאה למתודה הדרוסה של Dog)
myDog.makeSound(); // Output: Dog barks!
myDog.fetch(); // Output: Dog fetches.
}
}
בדוגמה זו, למרות ש-myDogAsAnimal מוגדר כ-Animal, בזמן ריצה קריאה ל-makeSound() תפעיל את היישום של Dog, הודות למנגנון הפולימורפיזם והמתודות הווירטואליות.
ירושה (Inheritance)
מנגנון "IS-A" המאפשר למחלקה לרשת שדות ומתודות ממחלקה אחרת. מקדם שימוש חוזר בקוד ויוצר היררכיות.
קריאה לבנאי אב (super())
חובה להפעיל את בנאי האב מתוך בנאי הבן (במפורש או במרומז) כדי לאתחל את חלק האב באובייקט. חייבת להיות השורה הראשונה.
דריסת מתודה (Override)
מתן יישום חדש למתודה קיימת במחלקת האב, תוך שמירה על אותה חתימה. מאפשר פולימורפיזם. השתמשו ב-@Override.
שאלות לדיון
- הסבירו מדוע ירושה נחשבת לאחד מעמודי התווך של תכנות מונחה עצמים. אילו יתרונות עיקריים היא מציעה?
- מה ההבדל בין קריאה מפורשת ל-
super()לבין קריאה מרומזת? מתי נהיה חייבים להשתמש בקריאה מפורשת? - תארו מצב שבו תעדיפו להשתמש בדריסת מתודה (override) על פני יצירת מתודה חדשה במחלקת הבן.
- האם ניתן לדרוס מתודה שהוגדרה כ-
privateבמחלקת האב? נמקו.
נקודות לתשובת מודל
- יתרונות ירושה: שימוש חוזר בקוד, פולימורפיזם, הרחבת פונקציונליות, בניית היררכיות לוגיות ברורות, הפשטה.
- קריאה ל-
super(): קריאה מרומזת מתרחשת אוטומטית לבנאי ברירת המחדל (ללא פרמטרים) של האב. קריאה מפורשת נדרשת כאשר לאב אין בנאי ברירת מחדל, או כאשר רוצים להפעיל בנאי ספציפי של האב המקבל פרמטרים. - דריסת מתודה: כאשר מחלקת הבן צריכה לספק התנהגות שונה או ספציפית יותר למתודה שכבר קיימת באב, אך עדיין לשמור על אותה חתימה ויכולת פולימורפית. לדוגמה, מתודת
draw()במחלקתShapeשתקבל יישומים שונים ב-Circleו-Rectangle. - דריסת מתודות
private: לא ניתן לדרוס מתודותprivate. מתודותprivateאינן חלק מממשק המחלקה ואינן נגישות למחלקות הבן, ולכן אינן ניתנות לדריסה. אם מחלקת הבן תגדיר מתודה עם אותה חתימה, זו תיחשב למתודה חדשה לחלוטין, ולא לדריסה.