אני רק שאלה

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

public interface MyInterface {
void f();
}

ונתונה המחלקה הבאה:

public abstract class A implements MyInterface {
public A() {
System.out.println(“In A”);
f();
}
}

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

public abstract void f();

לא שזאת היתה טעות לכתוב ככה, אבל זה לא נחוץ, כיוון שממילא כל שיטה שמוגדרת בממשק היא כבר ציבורית ומופשטת מעצם היותה בתוך הממשק.
כעת נעבור למחלקה A. מחלקות יכולות לממש ממשקים ע”י המילה implements. מימוש הממשק ע”י מחלקה הוא אחד המאפיינים החזקים של ג’אווה. ראשית, מימוש הממשק מחייב את המחלקה לממש (לדרוס) את כל השיטות שהממשק מגדיר. ספציפית, הממשק שלנו MyInterface מכיל שיטה אחת בשם f, וכל מחלקה שמממשת את הממשק מחוייבת לדרוס שיטה זו. בנוסף, מחלקה שמממשת ממשק מקבלת את הממשק כ”אב חורג” שלה, כלומר לצרכי פולימורפיזם, ניתן להתייחס לאובייקטים מסוג המחלקה באמצעות משתנה מסוג הממשק. אבל זה שוב לא הנושא כרגע, כי כרגע אני רוצה להתמקד באלמנט הטכני המוזר במקצת של המחלקה A.
אנו רואים שהמחלקה A מממשת את הממשק MyInterface, אולם המחלקה לא דורסת את השיטה f. האם זאת לא סתירה לעובדה שכל מחלקה שמממשת ממשק חייבת לדרוס את השיטות שלו? שימו לב שהמחלקה A מוגדרת כמחלקה מופשטת בעצמה, וזה המפתח לשאלה הזאת – כיוון שהיא בעצמה מחלקה מופשטת, היא לא מחוייבת לממש שום דבר. היא אכן מממשת את הממשק, אבל חובת המימוש של השיטה f לא מוטלת עליה, כיוון שהיא מופשטת.
אבל עכשיו מגיע מצב שנראה מוזר לחלוטין וגם לא חוקי – בבנאי של המחלקה A יש קריאה לשיטה f. השיטה שמעולם לא מימשנו ובעצם אין שום קוד שלה בשום מקום. איך זה יכול להיות? או במילים אחרות, למה זה עובר קומפילציה?
בשביל להבין את זה צריך לזכור את השתלשלות העניינים שתגרום לבנאי של A להיקרא, תרחיש שגם הקומפיילר מכיר – כיוון שהמחלקה מופשטת, בשום אופן לא נוכל לכתוב שורה מהסוג הזה –

A a1 = new A();

אין אפשרות ליצור אובייקטים ממחלקה מופשטת, ולכן הפקודה new A() אינה חוקית ולא תעבור קומפילציה. לכן אין שום סיכוי שהבנאי של A יקרא כאשר רק המחלקה A בנמצא.
מתי, אם ככה, הבנאי יקרא? כמו כל היררכיית ירושה, הבנאי של המחלקה המורישה יקרא כאשר הבנאי של מחלקה יורשת יקרא. כלומר, אם תהיה מחלקה B למשל, שיורשת מ-A, הבנאי שלה יוכל לקרוא לבנאי של A.
ועכשיו הנה הקלוז’ור של כל העניין – אז בסדר, הבנאי של המחלקה היורשת יעשה את העבודה ויקרא לבנאי של A, האם אנחנו לא באותה בעיה? בואו נראה איך נראית המחלקה היורשת – למחלקה B שיורשת מ-A יש שתי אפשרויות – או שהיא תהיה מופשטת או שלא.
נניח ש-B אינה מופשטת. כלומר כך –

public class B extends A {}

הקוד הזה כפי שכתוב, לא יעבור קומפילציה. למה? כיוון שבמחלקה B יש עכשיו שיטה מופשטת אחת שלא מומשה בשום מקום. רואים איזו שיטה זו? זאת ידידתנו הותיקה, השיטה f שהמחלקה A לקחה לעצמה באמצעות מימוש הממשק, אבל לא מימשה, והעבירה למחלקה B באמצעות הירושה. המחלקה B תהיה חייבת לממש את השיטה f. כלומר, הקוד של B יראה משהו כזה –

public class B extends A {
public void f() { // ….. }
}

ועכשיו מה יקרה כאשר ניצור אובייקט מסוג B?

B b1 = new B();

הבנאי הריק של B (שימו לב – בנאי דיפולטיבי אבל עדיין בנאי!) יקרא לבנאי הריק של A. הבנאי של A יקרא לשיטה f, ולפי חוקי הפולימורפיזם, תתבצע פה קשירה דינמית, והשיטה התחתונה ביותר תופעל, כלומר השיטה שמוגדרת במחלקה B. מהסיבה הזאת (אמנם סיבה פתלתלה וארוכה) הקומפיילר מעביר את המחלקה A קומפילציה. נחזור רגע על התרחיש –
1. הקומפיילר יודע שבמחלקה A אין מימוש לשיטה f אבל אין בעיה עם זה כי המחלקה מופשטת.
2. הקומפיילר יודע לכן שהקריאה לשיטה f מתוך הבנאי של A לא תקרה לעולם עבור אובייקט מסוג A בלבד.
3. הקומפיילר גם יודע שהדרך היחידה להפעיל את הבנאי של A היא לקרוא לו מתוך בנאי של הבן של A, והבן של A, אם הוא לא מופשט בעצמו, יהיה חייב לממש את f.
4. ובשלב הזה, בזמן ריצה ממילא השיטה “הנכונה” תופעל, אז הקומפיילר שם וי אחד גדול על כל העניין והולך לישון בשקט.
ושני דברים קטנים לקראת סיום –
ראשית, אובייקטים מסוג B אפשר ליצור גם ככה –

A b2 = new B();

וגם ככה –

MyInterface b3 = new B();

ואפילו ככה –

Object b4 = new B();

בכל המקרים מדובר באובייקט מסוג B, פשוט ההתייחסות אליו היא כל פעם דרך “אבא” אחר, או אם תרצו, דרך שם משפחה אחר.
ודבר נוסף, וזה אני משאיר לכם למצוא את התשובה – אמרתי שהמחלקה שיורשת מ-A יכולה להיות מופשטת או לא מופשטת, והראיתי מה יקרה כאשר היא תהיה לא מופשטת. אבל מה יקרה אם היא תהיה מופשטת? מה יגיד הקומפיילר אז? האם זה ישנה?
בהצלחה לכולם בבחינות (-;