Java-Polymorphismus und seine Typen

Polymorphismus bezieht sich auf die Fähigkeit einiger Entitäten, in verschiedenen Formen aufzutreten. Es wird im Volksmund durch den Schmetterling dargestellt, der sich von Larve über Puppe zu Imago verwandelt. Polymorphismus gibt es auch in Programmiersprachen als Modellierungstechnik, mit der Sie eine einzige Schnittstelle zu verschiedenen Operanden, Argumenten und Objekten erstellen können. Java-Polymorphismus führt zu Code, der präziser und einfacher zu warten ist.

Während sich dieses Tutorial auf den Subtyp-Polymorphismus konzentriert, gibt es einige andere Typen, die Sie kennen sollten. Wir beginnen mit einem Überblick über alle vier Arten von Polymorphismus.

download Code abrufen Laden Sie den Quellcode herunter, zum Beispiel Anwendungen in diesem Tutorial. Erstellt von Jeff Friesen für JavaWorld.

Arten von Polymorphismus in Java

In Java gibt es vier Arten von Polymorphismus:

  1. Zwang ist eine Operation, die durch implizite Typkonvertierung mehrere Typen bedient. Beispielsweise teilen Sie eine Ganzzahl durch eine andere Ganzzahl oder einen Gleitkommawert durch einen anderen Gleitkommawert. Wenn ein Operand eine Ganzzahl und der andere Operand ein Gleitkommawert ist, erzwingt der Compiler die Ganzzahl (impliziert sie implizit) in einen Gleitkommawert, um einen Typfehler zu vermeiden. (Es gibt keine Divisionsoperation, die einen Ganzzahloperanden und einen Gleitkommaoperanden unterstützt.) Ein weiteres Beispiel ist die Übergabe einer Unterklassenobjektreferenz an den Oberklassenparameter einer Methode. Der Compiler erzwingt den Unterklassentyp zum Oberklassentyp, um Operationen auf die der Oberklasse zu beschränken.
  2. Überladen bezieht sich auf die Verwendung desselben Operatorsymbols oder Methodennamens in verschiedenen Kontexten. +Abhängig von den Typen der Operanden können Sie beispielsweise eine Ganzzahladdition, eine Gleitkommaaddition oder eine Zeichenfolgenverkettung durchführen. Außerdem können in einer Klasse mehrere Methoden mit demselben Namen angezeigt werden (durch Deklaration und / oder Vererbung).
  3. Der parametrische Polymorphismus legt fest, dass innerhalb einer Klassendeklaration ein Feldname verschiedenen Typen und ein Methodenname verschiedenen Parameter- und Rückgabetypen zugeordnet werden kann. Das Feld und die Methode können dann in jeder Klasseninstanz (Objekt) unterschiedliche Typen annehmen. Beispielsweise kann ein Feld vom Typ sein Double(ein Mitglied der Standardklassenbibliothek von Java, das einen doubleWert umschließt ), und eine Methode kann ein Doublein einem Objekt zurückgeben, und dasselbe Feld kann vom Typ sein Stringund dieselbe Methode kann ein Stringin einem anderen Objekt zurückgeben . Java unterstützt den parametrischen Polymorphismus über Generika, auf die ich in einem zukünftigen Artikel eingehen werde.
  4. Subtyp bedeutet, dass ein Typ als Subtyp eines anderen Typs dienen kann. Wenn eine Subtypinstanz in einem Supertypkontext angezeigt wird, führt das Ausführen einer Supertypoperation für die Subtypinstanz dazu, dass die Version des Subtyps dieser Operation ausgeführt wird. Stellen Sie sich beispielsweise ein Codefragment vor, das beliebige Formen zeichnet. Sie können diesen Zeichnungscode präziser ausdrücken, indem Sie eine ShapeKlasse mit einer draw()Methode einführen . durch die Einführung Circle, Rectangleund andere Subklassen außer Kraft draw(); durch Einführen eines Arrays vom Typ, Shapedessen Elemente Verweise auf ShapeUnterklasseninstanzen speichern ; und durch Aufrufen Shapeder draw()Methode für jede Instanz. Wenn Sie anrufen draw(), sind es die Circle's, Rectangle' s oder andere ShapeInstanzendraw()Methode, die aufgerufen wird. Wir sagen , dass es viele Formen von Shape‚s - draw()Methode.

Dieses Tutorial führt in den Subtyp-Polymorphismus ein. Sie lernen Upcasting und Late Binding, abstrakte Klassen (die nicht instanziiert werden können) und abstrakte Methoden (die nicht aufgerufen werden können) kennen. Außerdem lernen Sie Downcasting und die Identifizierung von Laufzeit-Typen kennen und erhalten einen ersten Einblick in kovariante Rückgabetypen. Ich werde den parametrischen Polymorphismus für ein zukünftiges Tutorial speichern.

Ad-hoc vs. universeller Polymorphismus

Wie viele Entwickler klassifiziere ich Zwang und Überladung als Ad-hoc-Polymorphismus und parametrisch und subtypisch als universellen Polymorphismus. Obwohl wertvolle Techniken, glaube ich nicht, dass Zwang und Überlastung wahrer Polymorphismus sind; Sie ähneln eher Typumwandlungen und syntaktischem Zucker.

Subtyp-Polymorphismus: Upcasting und späte Bindung

Der Subtyp-Polymorphismus beruht auf Upcasting und später Bindung. Upcasting ist eine Form des Castings, bei der Sie die Vererbungshierarchie von einem Subtyp in einen Supertyp umwandeln. Es ist kein Cast-Operator beteiligt, da der Subtyp eine Spezialisierung des Supertyps ist. Zum Beispiel Shape s = new Circle();Upcasts von Circlebis Shape. Dies ist sinnvoll, da ein Kreis eine Art Form ist.

Nach dem Upcasting Circleauf Shapekönnen Sie keine Circle-spezifischen Methoden aufrufen , z. B. eine getRadius()Methode, die den Radius des Kreises zurückgibt, da Circle-spezifische Methoden nicht Teil der ShapeSchnittstelle sind. Der Zugriff auf Subtyp-Features zu verlieren, nachdem eine Unterklasse auf ihre Oberklasse eingegrenzt wurde, erscheint sinnlos, ist jedoch erforderlich, um einen Subtyp-Polymorphismus zu erzielen.

Angenommen, Shapeeine draw()Methode wird deklariert , ihre CircleUnterklasse überschreibt diese Methode, Shape s = new Circle();wurde gerade ausgeführt und in der nächsten Zeile angegeben s.draw();. Welche draw()Methode heißt: Shape's draw()Methode oder Circle' s draw()Methode? Der Compiler weiß nicht, welche draw()Methode er aufrufen soll. Sie können lediglich überprüfen, ob eine Methode in der Oberklasse vorhanden ist, und sicherstellen, dass die Argumentliste und der Rückgabetyp des Methodenaufrufs mit der Methodendeklaration der Oberklasse übereinstimmen. Der Compiler fügt jedoch auch eine Anweisung in den kompilierten Code ein, der zur Laufzeit sdie richtige draw()Methode abruft und die darin enthaltenen Referenzen verwendet. Diese Aufgabe wird als späte Bindung bezeichnet .

Späte Bindung vs. frühe Bindung

Die späte Bindung wird für Aufrufe von Nichtinstanzmethoden finalverwendet. Bei allen anderen Methodenaufrufen weiß der Compiler, welche Methode aufgerufen werden soll. Es fügt eine Anweisung in den kompilierten Code ein, die die Methode aufruft, die dem Typ der Variablen und nicht ihrem Wert zugeordnet ist. Diese Technik ist als frühe Bindung bekannt .

Ich habe eine Anwendung erstellt, die den Subtyp-Polymorphismus in Bezug auf Upcasting und späte Bindung demonstriert. Diese Anwendung besteht aus Shape, Circle, Rectangleund ShapesKlassen, wobei jede Klasse in einer eigenen Quelldatei gespeichert ist. Listing 1 zeigt die ersten drei Klassen.

Listing 1. Deklarieren einer Formenhierarchie

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 + ")"); } }

Listing 2 zeigt die ShapesAnwendungsklasse, deren main()Methode die Anwendung steuert.

Listing 2. Upcasting und späte Bindung im Subtyp-Polymorphismus

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(); } }

Die Deklaration des shapesArrays zeigt Upcasting. Die Circleund RectangleReferenzen werden in shapes[0]und gespeichert und shapes[1]auf den Typ übertragen Shape. Jedes von shapes[0]und shapes[1]wird als eine ShapeInstanz angesehen: shapes[0]wird nicht als eine angesehen Circle; shapes[1]wird nicht als Rectangle.

Die späte Bindung wird durch die shapes[i].draw();Expression gezeigt. Wenn igleich 0, bewirkt der vom Compiler generierte Befehl, dass Circledie draw()Methode aufgerufen wird. Wenn igleich 1, führt dieser Befehl jedoch dazu, dass Rectangledie draw()Methode aufgerufen wird. Dies ist die Essenz des Subtyp-Polymorphismus.

Unter der Annahme , dass alle vier Quelldateien ( Shapes.java, Shape.java, Rectangle.javaund Circle.java) im aktuellen Verzeichnis befindet, kompilieren sie über eine der folgenden Befehlszeilen:

javac *.java javac Shapes.java

Führen Sie die resultierende Anwendung aus:

java Shapes

Sie sollten die folgende Ausgabe beachten:

Drawing circle (10, 20, 30) Drawing rectangle (20, 30, 40, 50)

Abstrakte Klassen und Methoden

Wenn Sie Klassenhierarchien entwerfen, werden Sie feststellen, dass Klassen, die sich am oberen Rand dieser Hierarchien befinden, allgemeiner sind als Klassen, die sich weiter unten befinden. Beispielsweise ist eine VehicleOberklasse allgemeiner als eine TruckUnterklasse. Ebenso ist eine ShapeOberklasse allgemeiner als eine Circleoder eine RectangleUnterklasse.

It doesn't make sense to instantiate a generic class. After all, what would a Vehicle object describe? Similarly, what kind of shape is represented by a Shape object? Rather than code an empty draw() method in Shape, we can prevent this method from being called and this class from being instantiated by declaring both entities to be abstract.

Java provides the abstract reserved word to declare a class that cannot be instantiated. The compiler reports an error when you try to instantiate this class. abstract is also used to declare a method without a body. The draw() method doesn't need a body because it is unable to draw an abstract shape. Listing 3 demonstrates.

Listing 3. Abstracting the Shape class and its draw() method

abstract class Shape { abstract void draw(); // semicolon is required }

Abstract cautions

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.

In diesem Fall beschwert sich der Compiler nicht, da das Downcasting von einer Oberklasse in eine Unterklasse in derselben Typhierarchie zulässig ist. Das heißt, wenn die Zuweisung erlaubt wäre, würde die Anwendung abstürzen, wenn sie versucht, ausgeführt zu werden subclass.method();. In diesem Fall würde die JVM versuchen, eine nicht vorhandene Methode aufzurufen, da Superclasssie nicht deklariert method(). Glücklicherweise überprüft die JVM, ob eine Besetzung legal ist, bevor eine Besetzungsoperation ausgeführt wird. Wenn Sie feststellen, dass Superclassdies nicht deklariert ist method(), wird ein ClassCastExceptionObjekt geworfen . (Ich werde Ausnahmen in einem zukünftigen Artikel besprechen.)

Kompilieren Sie Listing 5 wie folgt:

javac BadDowncast.java

Führen Sie die resultierende Anwendung aus:

java BadDowncast