Polymorphismus in Java

1. Übersicht

Alle OOP-Sprachen (Object-Oriented Programming) müssen vier grundlegende Merkmale aufweisen: Abstraktion, Kapselung, Vererbung und Polymorphismus.

In diesem Artikel werden zwei Kerntypen des Polymorphismus behandelt: statischer Polymorphismus oder Polymorphismus zur Kompilierungszeit und dynamischer Polymorphismus oder Laufzeitpolymorphismus . Statischer Polymorphismus wird zur Kompilierungszeit erzwungen, während dynamischer Polymorphismus zur Laufzeit realisiert wird.

2. Statischer Polymorphismus

Laut Wikipedia ist statischer Polymorphismus eine Nachahmung des Polymorphismus, der zur Kompilierungszeit aufgelöst wird und somit die Suche nach virtuellen Tabellen zur Laufzeit überflüssig macht .

Beispielsweise kann unsere TextFile- Klasse in einer Dateimanager-App drei Methoden mit derselben Signatur wie die read () -Methode haben:

public class TextFile extends GenericFile { //... public String read() { return this.getContent() .toString(); } public String read(int limit) { return this.getContent() .toString() .substring(0, limit); } public String read(int start, int stop) { return this.getContent() .toString() .substring(start, stop); } }

Während der Codekompilierung überprüft der Compiler, ob alle Aufrufe der Lesemethode mindestens einer der drei oben definierten Methoden entsprechen.

3. Dynamischer Polymorphismus

Bei dynamischem Polymorphismus übernimmt die Java Virtual Machine (JVM) die Erkennung der geeigneten Methode, die ausgeführt werden soll, wenn eine Unterklasse ihrem übergeordneten Formular zugewiesen wird . Dies ist erforderlich, da die Unterklasse möglicherweise einige oder alle in der übergeordneten Klasse definierten Methoden überschreibt.

Definieren wir in einer hypothetischen Dateimanager-App die übergeordnete Klasse für alle Dateien mit dem Namen GenericFile :

public class GenericFile { private String name; //... public String getFileInfo() { return "Generic File Impl"; } }

Wir können auch eine ImageFile- Klasse implementieren, die die GenericFile erweitert, aber die getFileInfo () -Methode überschreibt und weitere Informationen anfügt:

public class ImageFile extends GenericFile { private int height; private int width; //... getters and setters public String getFileInfo() { return "Image File Impl"; } }

Wenn wir eine Instanz erstellen Imagefile und Zuweisen zu einem GenericFile Klasse wird eine implizite Umwandlung getan. Die JVM behält jedoch einen Verweis auf die tatsächliche Form von ImageFile bei .

Das obige Konstrukt ist analog zum Überschreiben von Methoden. Wir können dies bestätigen, indem wir die Methode getFileInfo () aufrufen, indem wir :

public static void main(String[] args) { GenericFile genericFile = new ImageFile("SampleImageFile", 200, 100, new BufferedImage(100, 200, BufferedImage.TYPE_INT_RGB) .toString() .getBytes(), "v1.0.0"); logger.info("File Info: \n" + genericFile.getFileInfo()); }

Wie erwartet, genericFile.getFileInfo () löst die getFileInfo () Methode der Imagefile - Klasse in der Ausgabe , wie gesehen unten:

File Info: Image File Impl

4. Andere polymorphe Eigenschaften in Java

Zusätzlich zu diesen beiden Haupttypen des Polymorphismus in Java gibt es andere Merkmale in der Java-Programmiersprache, die Polymorphismus aufweisen. Lassen Sie uns einige dieser Eigenschaften diskutieren.

4.1. Zwang

Polymorpher Zwang befasst sich mit der impliziten Typkonvertierung durch den Compiler, um Typfehler zu vermeiden. Ein typisches Beispiel ist die Verkettung von Ganzzahlen und Zeichenfolgen:

String str = “string” + 2;

4.2. Überlastung des Bedieners

Operator- oder Methodenüberladung bezieht sich auf eine polymorphe Eigenschaft desselben Symbols oder Operators, die je nach Kontext unterschiedliche Bedeutungen (Formen) hat.

Zum Beispiel kann das Plus - Symbol (+) für mathematische Addition verwendet werden sowie String - Verkettung. In beiden Fällen bestimmt nur der Kontext (dh die Argumenttypen) die Interpretation des Symbols:

String str = "2" + 2; int sum = 2 + 2; System.out.printf(" str = %s\n sum = %d\n", str, sum);

Ausgabe:

str = 22 sum = 4

4.3. Polymorphe Parameter

Durch parametrischen Polymorphismus kann ein Name eines Parameters oder einer Methode in einer Klasse verschiedenen Typen zugeordnet werden. Im Folgenden finden Sie ein typisches Beispiel, in dem wir Inhalte als Zeichenfolge und später als Ganzzahl definieren :

public class TextFile extends GenericFile { private String content; public String setContentDelimiter() { int content = 100; this.content = this.content + content; } }

Es ist auch wichtig zu beachten, dass die Deklaration polymorpher Parameter zu einem Problem führen kann, das als Ausblenden von Variablen bezeichnet wird, bei dem eine lokale Deklaration eines Parameters immer die globale Deklaration eines anderen Parameters mit demselben Namen überschreibt.

Um dieses Problem zu lösen, ist es häufig ratsam, globale Referenzen wie dieses Schlüsselwort zu verwenden, um auf globale Variablen in einem lokalen Kontext zu verweisen.

4.4. Polymorphe Subtypen

Der polymorphe Subtyp ermöglicht es uns bequem, einem Typ mehrere Subtypen zuzuweisen und zu erwarten, dass alle Aufrufe des Typs die verfügbaren Definitionen im Subtyp auslösen.

Wenn wir beispielsweise eine Sammlung von GenericFiles haben und die Methode getInfo () für jede von ihnen aufrufen , können wir erwarten, dass die Ausgabe je nach Subtyp, von dem jedes Element in der Sammlung abgeleitet wurde, unterschiedlich ist:

GenericFile [] files = {new ImageFile("SampleImageFile", 200, 100, new BufferedImage(100, 200, BufferedImage.TYPE_INT_RGB).toString() .getBytes(), "v1.0.0"), new TextFile("SampleTextFile", "This is a sample text content", "v1.0.0")}; for (int i = 0; i < files.length; i++) { files[i].getInfo(); }

Der Subtyp-Polymorphismus wird durch eine Kombination aus Upcasting und später Bindung ermöglicht . Beim Upcasting wird die Vererbungshierarchie von einem Supertyp in einen Subtyp umgewandelt:

ImageFile imageFile = new ImageFile(); GenericFile file = imageFile;

The resulting effect of the above is that ImageFile-specific methods cannot be invoked on the new upcast GenericFile. However, methods in the subtype override similar methods defined in the supertype.

To resolve the problem of not being able to invoke subtype-specific methods when upcasting to a supertype, we can do a downcasting of the inheritance from a supertype to a subtype. This is done by:

ImageFile imageFile = (ImageFile) file;

Late bindingstrategy helps the compiler to resolve whose method to trigger after upcasting. In the case of imageFile#getInfo vs file#getInfo in the above example, the compiler keeps a reference to ImageFile‘s getInfo method.

5. Problems With Polymorphism

Let's look at some ambiguities in polymorphism that could potentially lead to runtime errors if not properly checked.

5.1. Type Identification During Downcasting

Recall that we earlier lost access to some subtype-specific methods after performing an upcast. Although we were able to solve this with a downcast, this does not guarantee actual type checking.

For example, if we perform an upcast and subsequent downcast:

GenericFile file = new GenericFile(); ImageFile imageFile = (ImageFile) file; System.out.println(imageFile.getHeight());

We notice that the compiler allows a downcast of a GenericFile into an ImageFile, even though the class actually is a GenericFile and not an ImageFile.

Consequently, if we try to invoke the getHeight() method on the imageFile class, we get a ClassCastException as GenericFile does not define getHeight() method:

Exception in thread "main" java.lang.ClassCastException: GenericFile cannot be cast to ImageFile

To solve this problem, the JVM performs a Run-Time Type Information (RTTI) check. We can also attempt an explicit type identification by using the instanceof keyword just like this:

ImageFile imageFile; if (file instanceof ImageFile) { imageFile = file; }

The above helps to avoid a ClassCastException exception at runtime. Another option that may be used is wrapping the cast within a try and catch block and catching the ClassCastException.

It should be noted that RTTI check is expensive due to the time and resources needed to effectively verify that a type is correct. In addition, frequent use of the instanceof keyword almost always implies a bad design.

5.2. Fragile Base Class Problem

According to Wikipedia, base or superclasses are considered fragile if seemingly safe modifications to a base class may cause derived classes to malfunction.

Let's consider a declaration of a superclass called GenericFile and its subclass TextFile:

public class GenericFile { private String content; void writeContent(String content) { this.content = content; } void toString(String str) { str.toString(); } }
public class TextFile extends GenericFile { @Override void writeContent(String content) { toString(content); } }

When we modify the GenericFile class:

public class GenericFile { //... void toString(String str) { writeContent(str); } }

Wir stellen fest, dass die obige Änderung TextFile in der Methode writeContent () in einer unendlichen Rekursion belässt , was schließlich zu einem Stapelüberlauf führt.

Um ein fragiles Basisklassenproblem zu beheben, können wir das Schlüsselwort final verwenden , um zu verhindern, dass Unterklassen die Methode writeContent () überschreiben . Eine ordnungsgemäße Dokumentation kann ebenfalls hilfreich sein. Und zu guter Letzt sollte die Zusammensetzung generell der Vererbung vorgezogen werden.

6. Fazit

In diesem Artikel haben wir das grundlegende Konzept des Polymorphismus diskutiert und uns dabei sowohl auf die Vor- als auch auf die Nachteile konzentriert.

Wie immer ist der Quellcode für diesen Artikel auf GitHub verfügbar.