ירושה היא שימוש חוזר מסוג "הוא-סוג-של"
ירושה מאפשרת למחלקה חדשה להיבנות על גבי מחלקה קיימת. המחלקה החדשה, תת-המחלקה, מקבלת אוטומטית את השדות והמתודות של מחלקת העל, ואז מוסיפה או משנה את מה שהיא צריכה. משתמשים בה כשטיפוס אחד הוא סוג מסוים יותר של טיפוס אחר: Dog הוא Animal, SavingsAccount הוא BankAccount.
כותבים אותה עם מילת המפתח extends. כל מה שמוגדר public או protected באב זמין בבן בלי לכתוב אותו מחדש.
Dog אף פעם לא מצהיר על name או על eat(), ובכל זאת יש לו את שניהם. זה כל העניין: התנהגות משותפת נמצאת במקום אחד.
super: פנייה למחלקת האב
בנאי של תת-מחלקה חייב לוודא שהאב מאותחל קודם. עושים את זה עם super(...), שקורא לבנאי של האב וחייב להיות הפקודה הראשונה בבנאי של תת-המחלקה. אם משמיטים אותו, Java מוסיפה בשקט קריאה לבנאי בלי הארגומנטים של האב, ואם לאב אין בנאי כזה, הקוד לא יתקמפל.
הפלט מראה שהבנאי של האב רץ לפני גוף הבנאי של הבן: הבנייה זורמת מראש ההיררכיה כלפי מטה.
דריסת מתודות
תת-מחלקה יכולה להחליף מתודה שירשה על ידי הגדרה מחדש שלה עם אותה חתימה. זו דריסה (overriding), וכדאי תמיד לסמן אותה עם @Override. האנוטציה לא חובה, אבל היא גורמת לקומפיילר לוודא שבאמת התאמתם למתודה של האב. היא תופסת שגיאות הקלדה כמו tostring() במקום toString(), שאחרת היו יוצרות בשקט מתודה חדשה לגמרי.
למרות שהמערך מוגדר מטיפוס Animal, כל איבר מריץ את ה-speak() שלו. Java בוחרת את המתודה לפי האובייקט האמיתי בזמן ריצה, לא לפי הטיפוס המוצהר של המשתנה. זה הבסיס של פולימורפיזם.
קריאה לגרסה של האב עם super.method()
דריסה לא חייבת לזרוק את העבודה של האב. השתמשו ב-super.method() כדי להריץ את הגרסה שעברה בירושה ואז להוסיף עליה:
בלי super., קריאה ל-log בתוך TimestampLogger.log הייתה קוראת לעצמה ונכנסת לרקורסיה אינסופית. super. אומר במפורש "הגרסה של האב".
שדות שעברו בירושה והרשאות גישה
תת-מחלקה רואה את האיברים ה-public וה-protected של האב, אבל לא את ה-private. שדות private עדיין קיימים באובייקט, והמתודות של האב עצמו יכולות לגשת אליהם, אבל תת-המחלקה לא יכולה להתייחס אליהם ישירות. השתמשו ב-protected, אחד ממגדירי הגישה של Java, כשאתם רוצים לתת גישה לתת-מחלקות ועדיין להסתיר את האיבר מקוד שאינו קשור.
class Base {
private int secret; // לא נראה לתת-מחלקות
protected int shared; // נראה לתת-מחלקות
}
class Derived extends Base {
void demo() {
shared = 5; // OK
// secret = 5; // שגיאת קומפילציה: private ב-Base
}
}
זו גם הסיבה שבנאי של תת-מחלקה חייב לעיתים קרובות לקרוא ל-super(...): זו הדרך היחידה לאתחל את המצב הפרטי של האב.
עצירת ירושה עם final
לפעמים אסור בכלל להרחיב מחלקה. String מוגדרת final בדיוק מהסיבה הזו. סימון מחלקה כ-final אוסר ליצור ממנה תת-מחלקות; סימון מתודה כ-final אוסר לדרוס אותה, אבל עדיין מאפשר להרחיב את המחלקה.
final class Constants { } // אי אפשר לרשת ממנה
class Config {
final void load() { } // תת-מחלקות יכולות להרחיב את Config
// אבל לא יכולות לדרוס את load()
}
השתמשו ב-final כשההתנהגות של מחלקה חייבת להיות מובטחת וקבועה בכל התוכנית. זה סימן מכוון של "לא להרחיב", לא ברירת המחדל.
מלכודת נפוצה: העדיפו הרכבה כשזה לא "הוא-סוג-של"
ירושה מפתה כי היא חוסכת קוד, אבל היא קושרת את הבן לאב בחוזקה. אם היחס הוא לא באמת "הוא-סוג-של", למשל Car שבמקרה צריך Engine, אל תכתבו Car extends Engine. למכונית יש מנוע, היא לא מנוע. תארו את זה עם שדה (הרכבה, composition) במקום:
class Car {
private Engine engine = new Engine(); // ל-Car יש Engine
void start() { engine.ignite(); }
}
השתמשו בירושה רק כשתת-המחלקה היא באמת גרסה מתמחה של מחלקת העל, ואתם רוצים גם לרשת את ההתנהגות שלה וגם להחליף אותה.
הבא בתור: ממשקים
ירושה עם extends נותנת לכם אב יחיד ומימוש משותף. אבל מחלקה יכולה להרחיב רק מחלקה אחת, אז איך נותנים יכולת משותפת למחלקות שאינן קשורות? בשביל זה יש ממשקים (interfaces): חוזה שמחלקות רבות יכולות לממש, והנושא של העמוד הבא.
שאלות נפוצות
מה זה ירושה ב-Java?
ירושה מאפשרת למחלקה אחת (תת-המחלקה) להשתמש מחדש בשדות ובמתודות של מחלקה אחרת (מחלקת העל) בעזרת מילת המפתח extends. תת-המחלקה מקבלת אוטומטית את האיברים ה-public וה-protected של האב, ויכולה להוסיף חדשים או להחליף התנהגות שירשה באמצעות דריסה. היא מתארת יחס "הוא-סוג-של": Dog הוא Animal.
מה עושה מילת המפתח super ב-Java?
super מפנה למחלקת האב. super(...) בתוך בנאי קורא לבנאי של האב (והוא חייב להיות הפקודה הראשונה), ואילו super.method() קורא לגרסה של האב של מתודה שדרסתם. כך תת-מחלקה יכולה להישען על הלוגיקה של האב במקום להחליף אותה לגמרי.
מה ההבדל בין דריסה (overriding) להעמסה (overloading) ב-Java?
דריסה מגדירה מחדש מתודה שעברה בירושה בתת-מחלקה עם אותה חתימה, ומשנה את ההתנהגות; סמנו אותה עם @Override. העמסה מגדירה כמה מתודות עם אותו שם אבל עם רשימות פרמטרים שונות באותה מחלקה. דריסה קשורה לירושה ולבחירת המתודה בזמן ריצה; העמסה היא פשוט שתי מתודות שבמקרה חולקות שם.