Menu

פולימורפיזם ב-Java: ממשק אחד, צורות רבות

איך פולימורפיזם ב-Java מאפשר למשתנה אחד להפנות לטיפוסים רבים, למה מתודות דרוסות נבחרות בזמן ריצה, ואיך משתמשים ב-upcasting, downcasting ו-instanceof בבטחה.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

הפניה אחת, טיפוסים רבים

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

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

גם a וגם b מוצהרים כ-Animal, ובכל זאת כל אחד מדפיס את הקול שלו. הבחירה הזו קורית בזמן ריצה, לפי האובייקט האמיתי.

Dynamic Method Dispatch

המנגנון שמאחורי זה נקרא dynamic method dispatch: עבור מתודת מופע דרוסה, ה-JVM מסתכלת על המחלקה של האובייקט בזמן ריצה כדי להחליט איזה מימוש להפעיל. המהדר רק בודק שהמתודה קיימת בטיפוס המוצהר; הבחירה בפועל נדחית עד שהתוכנית רצה.

זה מה שמאפשר ללולאה אחת לטפל בתערובת שלמה של טיפוסים בלי לשאול אף פעם מה כל אחד מהם:

הלולאה מכירה רק את Shape. הוסיפו בהמשך Triangle extends Shape, והקוד הזה ימשיך לעבוד בלי שינוי: זו כל הנקודה. הקוד תלוי בהפשטה, לא ברשימה הקונקרטית של הטיפוסים.

Upcasting ו-Downcasting

שמירת Dog במשתנה Animal היא upcasting: תנועה למעלה בהיררכיה לטיפוס כללי יותר. זה תמיד בטוח ו-Java עושה את זה במובלע, כי כל Dog הוא Animal.

הכיוון ההפוך הוא downcasting: לקחת הפניה מטיפוס הורה ולהתייחס אליה כתת-טיפוס מסוים. זו צורה של המרת טיפוסים בין טיפוסי הפניה, והיא תקפה רק אם האובייקט באמת מאותו תת-טיפוס, ולכן צריך לכתוב את ההמרה במפורש, ומסתכנים ב-ClassCastException אם טועים:

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

הגנה על downcast עם instanceof

לפני downcast, בדקו את הטיפוס האמיתי עם instanceof. Java מודרנית מאפשרת לקשור את התוצאה באותו ביטוי (pattern matching ל-instanceof), כך שמדלגים על ההמרה הנפרדת:

instanceof מחזיר false עבור null, כך שהבדיקה מגינה גם מפני NullPointerException. עם זאת, אם אתם מוצאים את עצמכם כותבים שרשראות ארוכות של instanceof, זה לעיתים קרובות סימן שההתנהגות שייכת בתוך המחלקות כמתודה דרוסה: תנו לפולימורפיזם לעשות את ההסתעפות בשבילכם.

Overriding מול Overloading

השניים נשמעים דומים אבל אין ביניהם קשר, וערבוב ביניהם הוא טעות קלאסית.

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

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

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

מלכודת נפוצה: שדות אינם פולימורפיים

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

p.name() מריצה את הגרסה של Child (פולימורפיזם), אבל p.label קורא את השדה של Parent, כי שדות מוסתרים ולא נדרסים. התיקון פשוט: שמרו שדות כ-private וגשו אליהם רק דרך מתודות, כך שהקריאה הפולימורפית תמיד מנצחת.

הבא: מגדירי גישה

פולימורפיזם עובד בצורה נקייה רק כשתת-מחלקות יכולות לראות ולדרוס את האיברים הנכונים, בזמן ששאר הקוד לא יכול להיכנס פנימה ולשבור אינווריאנטים. את האיזון הזה קובעים public, protected, private וגישת package-private: מגדירי הגישה (access modifiers), בעמוד הבא.

שאלות נפוצות

מה זה פולימורפיזם ב-Java?

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

מה ההבדל בין overriding ל-overloading ב-Java?

Overriding הוא כשתת-מחלקה מחליפה מתודה של מחלקת העל עם אותה חתימה, וזה מה שמניע פולימורפיזם בזמן ריצה. Overloading הוא כשלמחלקה אחת יש כמה מתודות עם אותו שם אבל רשימות פרמטרים שונות; המהדר בוחר אחת בזמן קומפילציה לפי הארגומנטים. Overriding מוכרע בזמן ריצה לפי טיפוס האובייקט; overloading מוכרע בזמן קומפילציה לפי טיפוסי הארגומנטים.

מה ההבדל בין upcasting ל-downcasting ב-Java?

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

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל