Smart-World Surf

יחידה 5: ירושה

לימוד מנגנון הירושה ב-Java ליצירת היררכיות של מחלקות.

ברוכים הבאים לשיעור על ירושה ב-Java! ירושה היא אחד מעמודי התווך של תכנות מונחה עצמים (OOP), המאפשרת לנו לבנות היררכיות של מחלקות, לקדם שימוש חוזר בקוד (Code Reusability) ולשפר את גמישות המערכת. בשיעור זה נצלול לעומק מנגנון הירושה ב-Java, נבין את עקרונותיו, את הכלים שהוא מעמיד לרשותנו ואת שיקולי התכנון החשובים.

עקרונות הירושה ב-Java

מהי ירושה?

ירושה היא מנגנון שבו מחלקה אחת (מחלקה יורשת / Subclass / Child Class) יכולה לרשת תכונות (שדות) והתנהגויות (שיטות) ממחלקה אחרת (מחלקה מורישה / Superclass / Parent Class). הקשר בין המחלקות הוא קשר "is-a" (לדוגמה, "כלב הוא סוג של חיה"). מטרתה העיקרית היא שימוש חוזר בקוד וארגון לוגי של המחלקות.

ירושה (Inheritance): מנגנון ב-OOP המאפשר למחלקה אחת לרשת שדות ושיטות ממחלקה אחרת, תוך יצירת קשר "is-a" והשגת שימוש חוזר בקוד.

מילת המפתח extends

ב-Java, אנו משתמשים במילת המפתח extends כדי להצהיר על ירושה. לדוגמה: class Dog extends Animal { ... }. מחלקה Dog תירש את כל השדות והשיטות הנגישים ממחלקת Animal.

ירושה יחידה

Java תומכת ב"ירושה יחידה" בלבד, כלומר, מחלקה יכולה לרשת רק ממחלקה אחת ישירות. עם זאת, היא יכולה לממש מספר ממשקים (Interfaces), כפי שנראה בהמשך.

מגדירי גישה (Access Modifiers)

מגדירי הגישה קובעים את נראותם של השדות והשיטות במחלקת האב עבור המחלקות היורשות:

  • public: נגיש לכולם.
  • protected: נגיש למחלקות יורשות ולכל המחלקות באותה חבילה (package).
  • default (ללא מילת מפתח): נגיש רק למחלקות באותה חבילה. אינו נגיש למחלקות יורשות בחבילות אחרות.
  • private: אינו נגיש מחוץ למחלקה עצמה. מחלקות יורשות אינן יכולות לגשת ישירות לשדות ושיטות private של מחלקת האב, אך הן יכולות להשתמש בהן בעקיפין דרך שיטות public או protected של האב.

רכיבי מפתח בירושה

מילת המפתח super

מילת המפתח super משמשת לשתי מטרות עיקריות:

  • קריאה לבנאי של מחלקת האב: כאשר מחלקה יורשת יוצרת מופע, היא חייבת לאתחל גם את חלק האב שלה. קריאה ל-super(...) בבנאי של מחלקת הבן מפעילה את הבנאי המתאים של מחלקת האב. קריאה זו חייבת להיות השורה הראשונה בבנאי של מחלקת הבן.
  • גישה לחברי מחלקת האב: ניתן להשתמש ב-super.method() או super.field כדי לגשת לשיטות או שדות של מחלקת האב, במיוחד כאשר הם עברו Overriding במחלקת הבן.

הבדלים קריטיים: Method Overriding vs. Method Overloading

Method Overriding

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

Method Overloading

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

פולימורפיזם (Polymorphism)

פולימורפיזם הוא היכולת של אובייקט לקבל צורות רבות. בהקשר של ירושה, הוא מתבטא ביכולת להתייחס לאובייקט של מחלקת בן כאל אובייקט של מחלקת אב (Upcasting), ובכך להפעיל עליו שיטות שמומשו במחלקת הבן (Overriding).

  • Upcasting: המרה בטוחה ואוטומטית של אובייקט ממחלקת בן למחלקת אב. לדוגמה: Animal a = new Dog();. הפניה a היא מטיפוס Animal, אך האובייקט בפועל הוא Dog.
  • Downcasting: המרה מפורשת של אובייקט ממחלקת אב למחלקת בן. לדוגמה: Dog d = (Dog) a;. פעולה זו דורשת בדיקה (לרוב באמצעות instanceof) ועלולה לגרום ל-ClassCastException אם האובייקט בפועל אינו מהטיפוס שאליו מנסים להמיר.
פולימורפיזם (Polymorphism): עקרון ב-OOP המאפשר לאובייקטים של מחלקות שונות להגיב באופן שונה לאותה קריאה לשיטה, כאשר הם נצפים דרך הפניה למחלקת האב המשותפת.

מחלקה Object

כל המחלקות ב-Java יורשות ממחלקת Object באופן מרומז. היא מספקת שיטות בסיסיות כמו equals(), hashCode(), ו-toString(), שניתן לעשות להן Overriding כדי לספק התנהגות ספציפית למחלקה.

מחלקות אבסטרקטיות וממשקים

מחלקה אבסטרקטית (Abstract Class)

מחלקה אבסטרקטית היא מחלקה שלא ניתן ליצור ממנה מופעים ישירות. היא יכולה להכיל שיטות אבסטרקטיות (ללא מימוש, המסומנות ב-abstract) וגם שיטות רגילות עם מימוש. מחלקות בנות שיורשות ממחלקה אבסטרקטית חייבות לממש את כל השיטות האבסטרקטיות, אלא אם כן הן גם אבסטרקטיות בעצמן. מחלקות אבסטרקטיות מתאימות למקרים של "is-a" כאשר יש מימוש חלקי משותף, אך לא ניתן או לא הגיוני לממש את כל ההתנהגויות במחלקת האב.

מחלקה אבסטרקטית (Abstract Class): מחלקה שאינה ניתנת ליצירת מופע ישיר, ויכולה להכיל שיטות ללא מימוש (אבסטרקטיות) המחייבות את המחלקות היורשות לממש אותן.

ממשק (Interface)

ממשק מגדיר חוזה של התנהגויות (שיטות) ללא מימוש (בגרסאות Java מוקדמות). מחלקה יכולה לממש מספר ממשקים באמצעות מילת המפתח implements. ממשקים מייצגים קשר "can-do" או "has-a-capability" (לדוגמה, "כלב יכול לרוץ"). הם מאפשרים פולימורפיזם ללא ירושה ישירה של קוד, ומהווים דרך להשיג "ירושה מרובה של טיפוסים" ב-Java.

מחלקה אבסטרקטית

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

ממשק

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

שיקולי תכנון וטעויות נפוצות

ירושה מול קומפוזיציה ("is-a" vs. "has-a")

זוהי החלטת תכנון מרכזית. ירושה מתאימה כאשר יש קשר "is-a" מובהק. קומפוזיציה (Composition), שבה מחלקה "מכילה" אובייקטים של מחלקות אחרות כשדות (קשר "has-a"), עדיפה לעיתים קרובות לשמירה על גמישות וצמצום תלות. לדוגמה, "מכונית מכילה מנוע" (קומפוזיציה) עדיף על "מכונית היא סוג של מנוע" (ירושה).

קומפוזיציה (Composition): עקרון תכנון שבו מחלקה מכילה מופעים של מחלקות אחרות כשדות, במקום לרשת מהן, ויוצרת קשר "has-a".

מילת המפתח final

  • final class: מונעת ירושה מהמחלקה.
  • final method: מונעת Overriding של השיטה במחלקות בנות.
  • final field: הופך שדה לקבוע (ניתן לאתחול פעם אחת בלבד).

ClassCastException

שגיאה זו מתרחשת בזמן ריצה כאשר מנסים לבצע Downcasting לא חוקי, כלומר, להמיר אובייקט לטיפוס שאינו הטיפוס האמיתי שלו או טיפוס יורש שלו. לדוגמה, אם Animal a = new Cat();, ניסיון Dog d = (Dog) a; יזרוק ClassCastException.

פולימורפיזם ו-Method Overriding: נושא זה הוא ליבת התכנות מונחה העצמים ומופיע רבות במבחנים. הבנה עמוקה של איך Java בוחרת איזו שיטה להפעיל בזמן ריצה (Dynamic Method Dispatch) כאשר עובדים עם הפניות למחלקות אב המצביעות על אובייקטים של מחלקות בנות, היא קריטית. שימו לב במיוחד להבדלים בין Overriding ל-Overloading ולמקרים בהם Downcasting נחוץ ומתי הוא מסוכן.

שאלות לדיון

  • הסבירו את הקשר "is-a" בהקשר של ירושה, ותנו דוגמה קונקרטית ב-Java. מדוע חשוב להקפיד על קשר זה בעת תכנון היררכיית מחלקות?
  • מה ההבדל העיקרי בין Method Overriding ל-Method Overloading? מתי נשתמש בכל אחד מהם, ומדוע טעות נפוצה לבלבל ביניהם?
  • הסבירו את עקרון הפולימורפיזם ב-Java ותנו דוגמה לקוד המדגים Upcasting ו-Downcasting. מה הסכנות הכרוכות ב-Downcasting?
  • מתי עדיף להשתמש במחלקה אבסטרקטית ומתי בממשק? תארו תרחיש שבו כל אחד מהם יהיה הבחירה הנכונה.
  • כיצד מילת המפתח super תורמת למנגנון הירושה, ובאילו הקשרים היא הכרחית?

נקודות לתשובת מודל

  • קשר "is-a": מחלקה יורשת היא "סוג של" מחלקה מורישה. לדוגמה, Dog *is a* Animal. חשוב לשמור על עקביות לוגית למניעת בעיות תכנון ושימוש שגוי במנגנון הירושה.
  • Overriding vs. Overloading: Overriding משנה מימוש של שיטה קיימת במחלקת אב (אותה חתימה). Overloading יוצר שיטות חדשות עם אותו שם אך חתימות שונות. הראשון קשור לפולימורפיזם, השני לגמישות בקריאה. הבלבול נובע מהשם הדומה אך הפונקציונליות השונה לחלוטין.
  • פולימורפיזם: היכולת להתייחס לאובייקט של מחלקת בן כאל אובייקט של מחלקת אב. Upcasting (Parent p = new Child();) בטוח ואוטומטי. Downcasting (Child c = (Child) p;) דורש בדיקה (instanceof) ועלול לזרוק ClassCastException אם הטיפוס בפועל אינו תואם, מה שמוביל לקריסת התוכנית.
  • מחלקה אבסטרקטית מול ממשק: אבסטרקטית: קשר "is-a" חזק, יכולה להכיל מימוש חלקי, ירושה יחידה. מתאימה להיררכיה עם התנהגות משותפת חלקית. ממשק: חוזה התנהגות ("can-do"), ללא מימוש (במקור), ירושה מרובה של טיפוסים. מתאים להגדרת יכולות שונות שאינן קשורות בהכרח להיררכיה.
  • שימוש ב-super: לקריאה לבנאי של מחלקת האב (חובה כשהבנאי של האב אינו בנאי ברירת מחדל), ולגישה מפורשת לשדות או שיטות של מחלקת האב שייתכן ועברו Overriding במחלקת הבן.
מצאתם טעות או שחסר משהו?
→ הקודמת
מערכים
הבאה ←
פולימורפיזם וממשקים