Polimorfismo in Java

1. Panoramica

Tutti i linguaggi di programmazione orientata agli oggetti (OOP) devono mostrare quattro caratteristiche di base: astrazione, incapsulamento, ereditarietà e polimorfismo.

In questo articolo, ci occupiamo di due tipi fondamentali di polimorfismo: statica o fase di compilazione polimorfismo e dinamici o runtime polimorfismo . Il polimorfismo statico viene applicato in fase di compilazione mentre il polimorfismo dinamico viene realizzato in fase di esecuzione.

2. Polimorfismo statico

Secondo Wikipedia, il polimorfismo statico è un'imitazione del polimorfismo che viene risolto in fase di compilazione e quindi elimina le ricerche di tabelle virtuali in fase di esecuzione .

Ad esempio, la nostra classe TextFile in un'app di gestione file può avere tre metodi con la stessa firma del metodo read () :

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

Durante la compilazione del codice, il compilatore verifica che tutte le chiamate del metodo di lettura corrispondano ad almeno uno dei tre metodi definiti sopra.

3. Polimorfismo dinamico

Con il polimorfismo dinamico, la Java Virtual Machine (JVM) gestisce il rilevamento del metodo appropriato da eseguire quando una sottoclasse viene assegnata al suo form genitore . Ciò è necessario perché la sottoclasse può sovrascrivere alcuni o tutti i metodi definiti nella classe genitore.

In un'ipotetica app di file manager, definiamo la classe genitore per tutti i file denominata GenericFile :

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

Possiamo anche implementare una classe ImageFile che estende GenericFile ma sovrascrive il metodo getFileInfo () e aggiunge ulteriori informazioni:

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

Quando creiamo un'istanza di ImageFile e la assegniamo a una classe GenericFile , viene eseguito un cast implicito. Tuttavia, la JVM mantiene un riferimento alla forma effettiva di ImageFile .

Il costrutto precedente è analogo alla sostituzione del metodo. Possiamo confermarlo invocando il metodo getFileInfo () tramite:

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

Come previsto, genericFile.getFileInfo () attiva il metodo getFileInfo () della classe ImageFile come mostrato nell'output di seguito:

File Info: Image File Impl

4. Altre caratteristiche polimorfiche in Java

Oltre a questi due tipi principali di polimorfismo in Java, ci sono altre caratteristiche nel linguaggio di programmazione Java che mostrano polimorfismo. Parliamo di alcune di queste caratteristiche.

4.1. Coercizione

La coercizione polimorfica si occupa della conversione del tipo implicita eseguita dal compilatore per prevenire errori di tipo. Un tipico esempio è visto in una concatenazione di numeri interi e stringhe:

String str = “string” + 2;

4.2. Sovraccarico dell'operatore

Il sovraccarico di operatori o metodi si riferisce a una caratteristica polimorfica dello stesso simbolo o operatore con significati (forme) diversi a seconda del contesto.

Ad esempio, il simbolo più (+) può essere utilizzato per l'addizione matematica e per la concatenazione di stringhe . In entrambi i casi, solo il contesto (cioè i tipi di argomento) determina l'interpretazione del simbolo:

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

Produzione:

str = 22 sum = 4

4.3. Parametri polimorfici

Il polimorfismo parametrico consente di associare un nome di un parametro o un metodo in una classe a tipi diversi. Abbiamo un tipico esempio di seguito in cui definiamo il contenuto come una stringa e successivamente come un numero intero :

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

È anche importante notare che la dichiarazione di parametri polimorfici può portare a un problema noto come occultamento di variabili in cui una dichiarazione locale di un parametro ha sempre la precedenza sulla dichiarazione globale di un altro parametro con lo stesso nome.

Per risolvere questo problema, è spesso consigliabile utilizzare riferimenti globali come questa parola chiave per puntare a variabili globali all'interno di un contesto locale.

4.4. Sottotipi polimorfici

Il sottotipo polimorfico ci rende convenientemente possibile assegnare più sottotipi a un tipo e aspettarci che tutte le invocazioni sul tipo attivino le definizioni disponibili nel sottotipo.

Ad esempio, se abbiamo una raccolta di GenericFile e invochiamo il metodo getInfo () su ciascuno di essi, possiamo aspettarci che l'output sia diverso a seconda del sottotipo da cui è stato derivato ciascun elemento nella raccolta:

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

Il polimorfismo del sottotipo è reso possibile da una combinazione di upcasting e late binding . L'upcasting implica il casting della gerarchia di ereditarietà da un supertipo a un sottotipo:

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

Osserviamo che la modifica precedente lascia TextFile in una ricorsione infinita nel metodo writeContent () , che alla fine si traduce in un overflow dello stack.

Per risolvere un fragile problema della classe base, possiamo usare la parola chiave final per impedire alle sottoclassi di sovrascrivere il metodo writeContent () . Anche una documentazione adeguata può aiutare. E, ultimo ma non meno importante, la composizione dovrebbe generalmente essere preferita all'eredità.

6. Conclusione

In questo articolo, abbiamo discusso il concetto fondamentale di polimorfismo, concentrandoci su vantaggi e svantaggi.

Come sempre, il codice sorgente di questo articolo è disponibile su GitHub.