פולימורפיזם מתייחס ליכולתם של ישויות מסוימות להתרחש בצורות שונות. זה מיוצג באופן פופולרי על ידי הפרפר, אשר משתנה מזחל לגולם לאימגו. פולימורפיזם קיים גם בשפות תכנות, כטכניקת דוגמנות המאפשרת ליצור ממשק יחיד לאופרות, טיעונים ואובייקטים שונים. פולימורפיזם ג'אווה גורם לקוד תמציתי יותר וקל יותר לתחזוקה.
בעוד שמדריך זה מתמקד בפולימורפיזם תת-סוג, ישנם עוד מספר סוגים שכדאי לדעת עליהם. נתחיל בסקירה של כל ארבעת סוגי הפולימורפיזם.
הורד קבל את הקוד הורד את קוד המקור למשל יישומים במדריך זה. נוצר על ידי ג'ף פרייזן עבור JavaWorld.סוגי פולימורפיזם בג'אווה
ישנם ארבעה סוגים של פולימורפיזם בג'אווה:
- כפייה היא פעולה המשרתת מספר סוגים באמצעות המרה מסוג משתמע. לדוגמה, אתה מחלק מספר שלם במספר שלם אחר או בערך נקודה צפה בערך בנקודה צפה אחרת. אם אופרנד אחד הוא מספר שלם והשני אופרנד הוא ערך נקודה צפה, המהדר מכריח (ממיר באופן מרומז) את המספר השלם לערך נקודה צפה כדי למנוע שגיאת סוג. (אין פעולת חלוקה שתומכת באופרנד שלם ובאופראנד של נקודה צפה.) דוגמה נוספת היא העברת התייחסות לאובייקט תת-מחלקה לפרמטר העל-מעמד של השיטה. המהדר מכריח את סוג המעמד המשנה לסוג מעמד העל כדי להגביל את הפעולות לאלו של מעמד העל.
- עומס יתר מתייחס לשימוש באותו סמל אופרטור או שם שיטה בהקשרים שונים. לדוגמה, ייתכן שתשתמש
+
בביצוע תוספת מספרים שלמים, תוספת נקודה צפה או שרשור מחרוזות, בהתאם לסוגי הפעולות שלה. כמו כן, מספר שיטות בעלות אותו שם יכולות להופיע בכיתה (באמצעות הצהרה ו / או ירושה). - פולימורפיזם פרמטרי קובע שבתוך הצהרת כיתות, שם שדה יכול לשייך לסוגים שונים ושם שיטה יכול לשייך לסוגים שונים של פרמטרים וחזרה. לאחר מכן השדה והשיטה יכולים לקבל סוגים שונים בכל מופע מחלקה (אובייקט). לדוגמא, שדה עשוי להיות מסוג
Double
(חבר בספריית המחלקות הסטנדרטית של Java שעוטףdouble
ערך) ושיטה עשויה להחזירDouble
אובייקט אחד, ואותו שדה יכול להיות מסוגString
ואותה שיטה עשויה להחזיר aString
באובייקט אחר. . ג'אווה תומכת בפולימורפיזם פרמטרי באמצעות גנריות, עליהם אדון במאמר עתידי. - תת-סוג פירושו שסוג יכול לשמש תת-סוג מסוג אחר. כאשר מופע תת-סוג מופיע בהקשר של סוג-על, ביצוע פעולת-על-סוג במופע-המשנה גורם לגרסת תת-הסוג של אותה פעולה. לדוגמה, שקול שבר של קוד המשרטט צורות שרירותיות. אתה יכול לבטא את קוד השרטוט הזה בצורה תמציתית יותר על ידי הצגת
Shape
כיתהdraw()
בשיטה; על ידי החדרהCircle
,Rectangle
, ו subclasses האחר שידרסוdraw()
; על ידי הצגת מערך מסוגShape
שהאלמנטים שלו מאחסנים הפניותShape
למופעי תת-מחלקה; ועל ידי קוראShape
של"draw()
השיטה על כל מופע. כשאתה מתקשרdraw()
, זה המקרים של 'Circle
,Rectangle
' או 'אחריםShape
'draw()
שיטה שמתקשרת. אנו אומרים כי יש צורות רבות שלShape
ים"draw()
שיטה.
מדריך זה מציג פולימורפיזם תת-סוג. תלמדו על העלאה ואגידה מאוחרת, שיעורים מופשטים (שלא ניתן לאינסטינציה) ושיטות מופשטות (שלא ניתן לקרוא להן). תוכלו ללמוד גם על ירידה בהפעלה וזיהוי מסוג זמן ריצה, ותקבלו מבט ראשון על סוגי החזרות משתנים. אני אשמור פולימורפיזם פרמטרי לצורך הדרכה עתידית.
אד-הוק מול פולימורפיזם אוניברסלי
כמו מפתחים רבים, אני מסווג כפייה ועומס יתר כפולימורפיזם אד-הוק, ופארמטרי ותת-סוג כפולימורפיזם אוניברסלי. אמנם טכניקות יקרות ערך אינני מאמינה שכפיה והעמסת יתר הן פולימורפיזם אמיתי; הם דומים יותר להמרות סוג וסוכר תחבירי.
פולימורפיזם של תת-סוג: הלהקה וכריכה מאוחרת
פולימורפיזם של תת-סוג מסתמך על העלאה וכריכה מאוחרת. Upcasting הוא סוג של ליהוק שבו אתה משליך את היררכיית הירושה מתת-סוג לסוג-על. אין מפעיל שחקנים מעורב מכיוון שתת-הסוג הוא התמחות של סוג-העל. לדוגמא, Shape s = new Circle();
עליות מ- Circle
to Shape
. זה הגיוני מכיוון שמעגל הוא סוג של צורה.
לאחר שידור Circle
ל- Shape
, אינך יכול להתקשר Circle
לשיטות ספציפיות, כגון getRadius()
שיטה המחזירה את רדיוס המעגל, מכיוון Circle
ששיטות ספציפיות אינן חלק Shape
מהממשק. איבוד הגישה לתכונות של תת-סוג לאחר צמצום תת-מחלקה למעמד-העל שלה נראה חסר טעם, אך הכרחי להשגת פולימורפיזם תת-סוג.
נניח Shape
שהצהרה על draw()
שיטה, Circle
מחלקה המשנה שלה עוקף שיטה זו, Shape s = new Circle();
הוצא זה עתה, והשורה הבאה מציינת s.draw();
. אילו draw()
השיטה נקראת: Shape
's draw()
שיטה או Circle
של draw()
השיטה? המהדר לא יודע לאיזו draw()
שיטה להתקשר. כל מה שהוא יכול לעשות הוא לוודא שקיימת מתודה בסופר-קלאס, ולוודא שרשימת הארגומנטים של סוג שיחת השיטה וסוג ההחזרה תואמים להצהרת השיטה של הסופר-קלאס. עם זאת, המהדר מכניס גם הוראה לקוד המהולל שבזמן הריצה מביא ומשתמש בכל התייחסות s
כדי לקרוא draw()
לשיטה הנכונה . משימה זו מכונה כריכה מאוחרת .
כריכה מאוחרת לעומת כריכה מוקדמת
איגוד מאוחר משמש לשיחות final
לשיטות שאינן מופיעות. בכל שאר קריאות השיטה, המהדר יודע לאיזו שיטה להתקשר. הוא מכניס הוראה לקוד המהולל שקורא לשיטה המשויכת לסוג המשתנה ולא לערכו. טכניקה זו ידועה ככריכה מוקדמת .
יצרתי יישום שמדגים פולימורפיזם תת-סוג במונחים של העלאה וכריכה מאוחרת. יישום זה מורכב Shape
, Circle
, Rectangle
, ו Shapes
כיתות, כאשר כל כיתה מאוחסן בקובץ מקור משלה. רישום 1 מציג את שלושת הכיתות הראשונות.
רישום 1. הצהרת היררכיית צורות
class Shape { void draw() { } } class Circle extends Shape { private int x, y, r; Circle(int x, int y, int r) { this.x = x; this.y = y; this.r = r; } // For brevity, I've omitted getX(), getY(), and getRadius() methods. @Override void draw() { System.out.println("Drawing circle (" + x + ", "+ y + ", " + r + ")"); } } class Rectangle extends Shape { private int x, y, w, h; Rectangle(int x, int y, int w, int h) { this.x = x; this.y = y; this.w = w; this.h = h; } // For brevity, I've omitted getX(), getY(), getWidth(), and getHeight() // methods. @Override void draw() { System.out.println("Drawing rectangle (" + x + ", "+ y + ", " + w + "," + h + ")"); } }
רישום 2 מציג את Shapes
מחלקת היישומים ששיטתה main()
מניעה את היישום.
רישום 2. upcasting וכריכה מאוחרת בפולימורפיזם תת-סוג
class Shapes { public static void main(String[] args) { Shape[] shapes = { new Circle(10, 20, 30), new Rectangle(20, 30, 40, 50) }; for (int i = 0; i < shapes.length; i++) shapes[i].draw(); } }
הכרזת shapes
המערך מדגימה עדכונים. Circle
ו Rectangle
אזכור מאוחסן shapes[0]
ו shapes[1]
ו פונים כלפי מעלה להקליד Shape
. כל אחד shapes[0]
ו shapes[1]
נחשב Shape
למשל: shapes[0]
הוא לא נחשב Circle
; shapes[1]
אינו נתפס כ- Rectangle
.
קשירה מאוחרת מודגמת על ידי shapes[i].draw();
הביטוי. כאשר i
שווה 0
, גורמי ההוראה שנוצר המהדר Circle
של draw()
שיטה להיקרא. כאשר i
שווה 1
, לעומת זאת, גורמי הוראה זו Rectangle
של draw()
שיטה להיקרא. זוהי המהות של פולימורפיזם תת-סוג.
בהנחה שכל קבצי מקור ארבע ( Shapes.java
, Shape.java
, Rectangle.java
, ו Circle.java
) ממוקמים בספרייה הנוכחית, ולעבד אותם באמצעות אחת משתי השורות הפקודה הבאה:
javac *.java javac Shapes.java
הפעל את היישום שהתקבל:
java Shapes
עליכם להתבונן בפלט הבא:
Drawing circle (10, 20, 30) Drawing rectangle (20, 30, 40, 50)
שיעורים ושיטות מופשטות
בעת תכנון היררכיות כיתתיות, תגלה כי שיעורים הקרובים יותר לראש ההיררכיות הללו הם כלליים יותר מאשר שיעורים הנמצאים למטה. לדוגמא, מעמד-על Vehicle
הוא גנרי יותר Truck
מתת - מחלקה. באופן דומה, מעמד-על Shape
הוא גנרי יותר מ- תת Circle
- Rectangle
מחלקה.
זה לא הגיוני להקים שיעור גנרי. אחרי הכל, מה Vehicle
יתאר אובייקט? באופן דומה, איזה סוג של צורה מיוצג על ידי Shape
אובייקט? במקום לקודד draw()
שיטה ריקה Shape
, אנו יכולים למנוע את קריאתה של שיטה זו ומייצרים מחלקה זו על ידי הכרזת שתי הישויות כמופשטות.
Java מספקת את abstract
המילה השמורה כדי להכריז על מחלקה שאינה ניתנת לאינסטינציה. המהדר מדווח על שגיאה כשאתה מנסה לייצר מחלקה זו. abstract
משמש גם כדי להכריז על שיטה ללא גוף. draw()
השיטה אינה זקוקה לגוף משום שהוא אינו מסוגל לצייר צורה מופשטת. רישום 3 מדגים.
רישום 3. הפשטת מחלקת הצורה ושיטת הציור שלה ()
abstract class Shape { abstract void draw(); // semicolon is required }
אזהרות מופשטות
The compiler reports an error when you attempt to declare a class abstract
and final
. For example, the compiler complains about abstract final class Shape
because an abstract class cannot be instantiated and a final class cannot be extended. The compiler also reports an error when you declare a method abstract
but don't declare its class abstract
. Removing abstract
from the Shape
class's header in Listing 3 would result in an error, for instance. This would be an error because a non-abstract (concrete) class cannot be instantiated when it contains an abstract method. Finally, when you extend an abstract class, the extending class must override all of the abstract methods, or else the extending class must itself be declared to be abstract; otherwise, the compiler will report an error.
An abstract class can declare fields, constructors, and non-abstract methods in addition to or instead of abstract methods. For example, an abstract Vehicle
class might declare fields describing its make, model, and year. Also, it might declare a constructor to initialize these fields and concrete methods to return their values. Check out Listing 4.
Listing 4. Abstracting a vehicle
abstract class Vehicle { private String make, model; private int year; Vehicle(String make, String model, int year) { this.make = make; this.model = model; this.year = year; } String getMake() { return make; } String getModel() { return model; } int getYear() { return year; } abstract void move(); }
You'll note that Vehicle
declares an abstract move()
method to describe the movement of a vehicle. For example, a car rolls down the road, a boat sails across the water, and a plane flies through the air. Vehicle
's subclasses would override move()
and provide an appropriate description. They would also inherit the methods and their constructors would call Vehicle
's constructor.
Downcasting and RTTI
Moving up the class hierarchy, via upcasting, entails losing access to subtype features. For example, assigning a Circle
object to Shape
variable s
means that you cannot use s
to call Circle
's getRadius()
method. However, it's possible to once again access Circle
's getRadius()
method by performing an explicit cast operation like this one: Circle c = (Circle) s;
.
This assignment is known as downcasting because you are casting down the inheritance hierarchy from a supertype to a subtype (from the Shape
superclass to the Circle
subclass). Although an upcast is always safe (the superclass's interface is a subset of the subclass's interface), a downcast isn't always safe. Listing 5 shows what kind of trouble could ensue if you use downcasting incorrectly.
Listing 5. The problem with downcasting
class Superclass { } class Subclass extends Superclass { void method() { } } public class BadDowncast { public static void main(String[] args) { Superclass superclass = new Superclass(); Subclass subclass = (Subclass) superclass; subclass.method(); } }
Listing 5 presents a class hierarchy consisting of Superclass
and Subclass
, which extends Superclass
. Furthermore, Subclass
declares method()
. A third class named BadDowncast
provides a main()
method that instantiates Superclass
. BadDowncast
then tries to downcast this object to Subclass
and assign the result to variable subclass
.
במקרה זה המהדר לא יתלונן מכיוון שההפלה משכבת-על למחלקה-משנה באותה היררכיה מסוג זה היא חוקית. עם זאת, אם ההקצאה הייתה מותרת היישום יקרוס כשניסה לבצע subclass.method();
. במקרה זה ה- JVM ינסה לקרוא לשיטה לא קיימת, משום Superclass
שאינו מצהיר method()
. למרבה המזל, ה- JVM מוודא כי צוות השחקנים הוא חוקי לפני ביצוע מבצע הליהוק. גילוי Superclass
שאינו מצהיר method()
, זה היה זורק ClassCastException
חפץ. (אדון בחריגים במאמר עתידי.)
הידור רישום 5 כדלקמן:
javac BadDowncast.java
הפעל את היישום שהתקבל:
java BadDowncast