פולימורפיזם ג'אווה וסוגיו

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

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

הורד קבל את הקוד הורד את קוד המקור למשל יישומים במדריך זה. נוצר על ידי ג'ף פרייזן עבור JavaWorld.

סוגי פולימורפיזם בג'אווה

ישנם ארבעה סוגים של פולימורפיזם בג'אווה:

  1. כפייה היא פעולה המשרתת מספר סוגים באמצעות המרה מסוג משתמע. לדוגמה, אתה מחלק מספר שלם במספר שלם אחר או בערך נקודה צפה בערך בנקודה צפה אחרת. אם אופרנד אחד הוא מספר שלם והשני אופרנד הוא ערך נקודה צפה, המהדר מכריח (ממיר באופן מרומז) את המספר השלם לערך נקודה צפה כדי למנוע שגיאת סוג. (אין פעולת חלוקה שתומכת באופרנד שלם ובאופראנד של נקודה צפה.) דוגמה נוספת היא העברת התייחסות לאובייקט תת-מחלקה לפרמטר העל-מעמד של השיטה. המהדר מכריח את סוג המעמד המשנה לסוג מעמד העל כדי להגביל את הפעולות לאלו של מעמד העל.
  2. עומס יתר מתייחס לשימוש באותו סמל אופרטור או שם שיטה בהקשרים שונים. לדוגמה, ייתכן שתשתמש +בביצוע תוספת מספרים שלמים, תוספת נקודה צפה או שרשור מחרוזות, בהתאם לסוגי הפעולות שלה. כמו כן, מספר שיטות בעלות אותו שם יכולות להופיע בכיתה (באמצעות הצהרה ו / או ירושה).
  3. פולימורפיזם פרמטרי קובע שבתוך הצהרת כיתות, שם שדה יכול לשייך לסוגים שונים ושם שיטה יכול לשייך לסוגים שונים של פרמטרים וחזרה. לאחר מכן השדה והשיטה יכולים לקבל סוגים שונים בכל מופע מחלקה (אובייקט). לדוגמא, שדה עשוי להיות מסוג Double(חבר בספריית המחלקות הסטנדרטית של Java שעוטף doubleערך) ושיטה עשויה להחזיר Doubleאובייקט אחד, ואותו שדה יכול להיות מסוג Stringואותה שיטה עשויה להחזיר a Stringבאובייקט אחר. . ג'אווה תומכת בפולימורפיזם פרמטרי באמצעות גנריות, עליהם אדון במאמר עתידי.
  4. תת-סוג פירושו שסוג יכול לשמש תת-סוג מסוג אחר. כאשר מופע תת-סוג מופיע בהקשר של סוג-על, ביצוע פעולת-על-סוג במופע-המשנה גורם לגרסת תת-הסוג של אותה פעולה. לדוגמה, שקול שבר של קוד המשרטט צורות שרירותיות. אתה יכול לבטא את קוד השרטוט הזה בצורה תמציתית יותר על ידי הצגת Shapeכיתה draw()בשיטה; על ידי החדרה Circle, Rectangle, ו subclasses האחר שידרסו draw(); על ידי הצגת מערך מסוג Shapeשהאלמנטים שלו מאחסנים הפניות Shapeלמופעי תת-מחלקה; ועל ידי קורא Shapeשל" draw()השיטה על כל מופע. כשאתה מתקשר draw(), זה המקרים של ' Circle, Rectangle' או 'אחרים Shape'draw()שיטה שמתקשרת. אנו אומרים כי יש צורות רבות של Shapeים" draw()שיטה.

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

אד-הוק מול פולימורפיזם אוניברסלי

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

פולימורפיזם של תת-סוג: הלהקה וכריכה מאוחרת

פולימורפיזם של תת-סוג מסתמך על העלאה וכריכה מאוחרת. Upcasting הוא סוג של ליהוק שבו אתה משליך את היררכיית הירושה מתת-סוג לסוג-על. אין מפעיל שחקנים מעורב מכיוון שתת-הסוג הוא התמחות של סוג-העל. לדוגמא, Shape s = new Circle();עליות מ- Circleto 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