Polimorfismo in Java
Scrivi codice Java flessibile con il polimorfismo compile-time (overloading) e quello runtime (overriding).
Il polimorfismo è "un'interfaccia, molte implementazioni". In Java si presenta in due forme:
- Polimorfismo compile-time (overloading): il compilatore sceglie tra metodi con lo stesso nome in base ai tipi degli argomenti passati.
- Polimorfismo runtime (overriding): la JVM sceglie tra le implementazioni dei metodi in base all'oggetto effettivo su cui viene effettuata la chiamata.
Il tipo runtime è quello che si intende di solito per "polimorfismo" in un contesto OOP, ed è quello che rende utile l'ereditarietà. Senza di esso, un riferimento Cat sarebbe l'unico modo per chiamare Cat.speak() — non si potrebbe scrivere codice che cicla su una lista mista di animali e chiede a ciascuno di parlare.
Polimorfismo compile-time — overloading
Due metodi nella stessa classe possono condividere un nome purché le loro liste di parametri differiscano. Il compilatore sceglie quale chiamare in base ai tipi degli argomenti nel punto di chiamata:
public class Printer {
void print(int n) { System.out.println("int: " + n); }
void print(double d) { System.out.println("double: " + d); }
void print(String s) { System.out.println("string: " + s); }
}
Printer p = new Printer();
p.print(5); // int
p.print(5.0); // double
p.print("hi"); // stringQuesto viene deciso interamente a compile-time. Il metodo scelto è incorporato nel bytecode; nulla cambia a runtime. L'overloading dei metodi è stato trattato in dettaglio in method overloading nella Parte 5.
Polimorfismo runtime — overriding e dynamic dispatch
Il tipo più interessante. Quando una sottoclasse fa l'override di un metodo, le chiamate tramite un riferimento di tipo padre vengono comunque dispacciare alla versione della sottoclasse:
class Animal {
String speak() { return "(noise)"; }
}
class Cat extends Animal {
@Override String speak() { return "meow"; }
}
class Dog extends Animal {
@Override String speak() { return "woof"; }
}
Animal[] zoo = { new Cat(), new Dog(), new Animal() };
for (Animal a : zoo) {
System.out.println(a.speak());
}
// meow
// woof
// (noise)Ad ogni iterazione a è tipizzato come Animal, ma l'oggetto effettivo è un Cat, un Dog o un Animal. La chiamata a.speak() non seleziona il metodo a compile-time — a compile-time il compilatore sa solo che a è un qualche Animal. A runtime, la JVM esamina l'oggetto effettivo e dispatch la chiamata allo speak della classe di quell'oggetto.
Questo è il dynamic dispatch (a volte chiamato virtual dispatch). È ciò che rende interessante il ciclo precedente: è scritto genericamente rispetto ad Animal, e funziona per qualsiasi sottoclasse — incluse quelle che non esistevano quando il ciclo è stato scritto.
Perché è importante
Il polimorfismo è la caratteristica OOP che rende il codice aperto all'estensione senza modifiche. Una funzione che prende una Shape e chiama area() su di essa funziona per ogni forma che esiste oggi e per ogni forma che qualcuno aggiunge domani. La funzione non ha bisogno di una catena if (shape instanceof Circle).
double totalArea(List<Shape> shapes) {
double sum = 0;
for (Shape s : shapes) sum += s.area(); // dispatches to each subclass
return sum;
}Aggiungendo Triangle extends Shape, totalArea funziona anche su liste di triangoli gratuitamente. Questa è la sostanza del Principio Open/Closed — aperto all'estensione, chiuso alla modifica.
Upcasting e downcasting
Passare da un tipo sottoclasse a un tipo padre è un upcast. È implicito e sempre sicuro:
Cat c = new Cat();
Animal a = c; // upcast — implicitAndare nell'altra direzione — assegnare un riferimento di tipo padre a un tipo sottoclasse — è un downcast. Richiede un'espressione di cast, e la JVM verifica a runtime che l'oggetto sia effettivamente di quel sottotipo:
Animal a = new Cat();
Cat c = (Cat) a; // downcast — runtime check
Animal a2 = new Dog();
Cat c2 = (Cat) a2; // ClassCastException at runtimeL'alternativa compatibile con il compile-time è il controllo instanceof, spesso combinato con il pattern matching nel Java moderno:
if (a instanceof Cat c) {
c.purr();
}I campi non sono polimorfici
Il dynamic dispatch si applica solo ai metodi di istanza. I campi, i metodi static e i metodi private sono legati a compile-time in base al tipo dichiarato del riferimento:
class A {
String label = "A";
static String klass() { return "A"; }
}
class B extends A {
String label = "B";
static String klass() { return "B"; }
}
A a = new B();
System.out.println(a.label); // "A" — field, not polymorphic
System.out.println(a.klass()); // "A" — static, not polymorphicQuesto è uno dei motivi per cui si mantengono i campi privati e si accede a essi tramite metodi — i metodi partecipano al polimorfismo; i campi no.
@Override e bug silenziosi
Annotare sempre gli override con @Override. L'annotazione dice al compilatore "questo è destinato a fare override di un metodo padre — fallisci se non lo fa". Senza di essa, un piccolo errore di battitura crea un nuovo metodo che sembra un override ma non lo è:
class Animal {
String speak() { return "(noise)"; }
}
class Cat extends Animal {
String Speak() { return "meow"; } // capital S — typo, new method
}
Animal a = new Cat();
System.out.println(a.speak()); // "(noise)" — Cat.Speak was never calledAggiungere @Override fa sì che il compilatore lo rilevi immediatamente.
Polimorfismo con le interfacce
L'ereditarietà non è l'unico modo. Un'interfaccia è anch'essa un tipo padre — classi concrete diverse la implementano, e il codice che accetta il tipo interfaccia funziona con tutte:
interface Greeter {
String greet();
}
class English implements Greeter {
public String greet() { return "Hello"; }
}
class French implements Greeter {
public String greet() { return "Bonjour"; }
}
Greeter g = new French();
System.out.println(g.greet()); // "Bonjour" — dispatched to French.greetStessa idea — scrivi codice rispetto all'astrazione, lascia che il runtime scelga l'implementazione. Il capitolo sulle interfacce approfondisce i meccanismi.
Un esempio pratico
Prossimi passi
Il polimorfismo si basa su un meccanismo: una sottoclasse che sostituisce un metodo ereditato. Quel meccanismo — cosa è consentito, cosa non lo è, e l'annotazione @Override che mantiene l'onestà — è l'argomento del prossimo capitolo. Continua con method overriding.