Ereditarietà in Java
Riutilizza ed estendi il comportamento delle classi in Java con la parola chiave extends e le regole dell'ereditarietà singola.
L'ereditarietà permette a una classe di costruirsi su un'altra invece di partire da zero. La nuova classe — la sottoclasse — ottiene tutti i campi e i metodi del genitore, e aggiunge (o sostituisce) solo ciò che è diverso. È il modo in cui Java esprime le relazioni "è-un": un Cat è un Animal, un AdminUser è un User.
La parola chiave è extends. Il meccanismo è semplice. La parte difficile — e di cui questo capitolo si occupa davvero — è riconoscere quando l'ereditarietà è lo strumento giusto e quando non lo è.
Un primo esempio
public class Animal {
String name;
void breathe() { System.out.println(name + " breathes"); }
}
public class Cat extends Animal {
void purr() { System.out.println(name + " purrs"); }
}Cat dichiara solo purr(). Ha comunque name e breathe(), perché li ha ereditati:
Cat c = new Cat();
c.name = "Mittens";
c.breathe(); // Mittens breathes — inherited from Animal
c.purr(); // Mittens purrs — declared on CatAnimal è la superclasse (o genitore), Cat è la sottoclasse (o figlia). Altri linguaggi le chiamano classe base/derivata.
Cosa viene ereditato
Una sottoclasse eredita:
- Tutti i campi e i metodi
publiceprotectedda ogni antenato. - I campi e i metodi package-private, se la sottoclasse si trova nello stesso package.
- Tutti i membri ereditati mantengono i loro modificatori originali.
Una sottoclasse non eredita:
- I costruttori. Non sono membri nello stesso senso — la sottoclasse ha bisogno dei propri.
- I campi e i metodi
private. Esistono nel genitore (il layout di memoria di unCatinclude comunque eventuali campiprivatediAnimal), ma la sottoclasse non può vederli per nome. L'unico modo per leggerli è tramite i metodi accessoripublic/protectedereditati.
Ereditarietà singola — un solo genitore
Ogni classe estende esattamente un'altra classe. Non esiste ereditarietà multipla per le classi in Java:
public class Hybrid extends Animal, Vehicle { } // ERROR — no multiple inheritancePuoi implementare molte interfacce (trattate in interfacce) — questo offre la flessibilità multi-sorgente che la maggior parte dei linguaggi usa per l'ereditarietà multipla, senza i problemi di ambiguità.
Se non scrivi extends per nulla, la classe estende implicitamente Object:
public class Foo { }
// is equivalent to
public class Foo extends Object { }Ecco perché ogni oggetto Java ha toString(), equals(), hashCode() e getClass() — sono tutti su Object. Il capitolo sulla classe Object li illustra nel dettaglio.
Classi che non possono essere estese
Una classe contrassegnata con final non può essere un genitore — tentare di estenderla è un errore di compilazione:
public final class Money { }
public class Coupon extends Money { } // ERROR — cannot inherit from final MoneyQuesta è una scelta deliberata, non una svista: molti tipi fondamentali sono final proprio perché nessuna sottoclasse possa cambiare il loro comportamento. String, Integer e le altre classi wrapper sono tutte final, il che contribuisce a renderle sicure da condividere e memorizzare nella cache. Quando vuoi un tipo che non può essere sottoclassato — di solito per garanzie di sicurezza o immutabilità — contrassegnalo con final.
Costruttori e super
Ogni costruttore di una sottoclasse deve, come prima azione, chiamare un costruttore del genitore. Se non scrivi la chiamata, Java inserisce super() automaticamente:
public class Animal {
String name;
public Animal(String name) { this.name = name; }
}
public class Cat extends Animal {
public Cat(String name) {
super(name); // call Animal(String)
}
}Se Animal non avesse un costruttore senza argomenti e Cat non scrivesse super(...), il compilatore si lamenterebbe — non c'è nessun Animal() da inserire implicitamente. Il capitolo sulla parola chiave super tratta a fondo il concatenamento dei costruttori con super(...) e le chiamate a super.method().
Override
Una sottoclasse può sostituire un metodo ereditato dichiarandone uno con la stessa firma:
public class Animal {
String speak() { return "(some noise)"; }
}
public class Cat extends Animal {
@Override
String speak() { return "meow"; }
}
Cat c = new Cat();
System.out.println(c.speak()); // meow@Override è un'annotazione che dice al compilatore "voglio che questo metodo faccia l'override di uno ereditato — segnala un errore se non è così." Usala sempre. Cattura errori di battitura e disallineamenti di firma che altrimenti creerebbero silenziosamente un nuovo metodo invece di sovrascrivere quello vecchio. Il capitolo sul method overriding copre tutte le regole.
Upcasting e polimorfismo
Un'istanza di una sottoclasse può essere assegnata a una variabile del tipo genitore:
Animal a = new Cat(); // upcast — implicit
a.speak(); // calls Cat's speak() — picked at runtimeQuesta è la base del polimorfismo, il capitolo successivo. La variabile a è di tipo Animal, ma l'oggetto reale è un Cat, quindi viene eseguita la versione speak di Cat.
Quando l'ereditarietà è lo strumento sbagliato
L'ereditarietà è il meccanismo più abusato nella OOP. Alcuni segnali d'allarme che indicano che dovresti usare la composizione (un campo di un altro tipo) invece:
- La sottoclasse non supera davvero il test "è-un". Uno
Stacknon è davvero unVector— eppurejava.util.StackestendeVectored è ampiamente considerato un errore di design. - Le variabili interne mutabili del genitore fuoriescono nella sottoclasse. Le modifiche all'implementazione del genitore rompono la sottoclasse.
- Stai ereditando per riutilizzare alcuni metodi, non perché i tipi siano genuinamente sostituibili.
La regola pratica di Joshua Bloch in Effective Java: preferisci la composizione all'ereditarietà. Se B ha bisogno del comportamento di A ma non è davvero un A, dai a B un campo privato di tipo A e delega ciò di cui ha bisogno.
// Inheritance — fragile
public class MyList extends ArrayList<String> { ... }
// Composition — robust
public class MyList {
private final List<String> inner = new ArrayList<>();
public void add(String s) { inner.add(s); }
}La versione con composizione è immune alle sorprese quando ArrayList aggiunge nuovi metodi o cambia il funzionamento dei suoi campi privati.
Ereditarietà e accesso
I membri ereditati mantengono il modificatore che avevano nel genitore. Una sottoclasse non può restringere la visibilità di un metodo sovrascritto — rendere private nella sottoclasse un metodo public violerebbe il principio di sostituzione di Liskov, e il compilatore lo rifiuta:
public class A {
public void hello() { }
}
public class B extends A {
private void hello() { } // ERROR — cannot reduce visibility
}Puoi ampliare la visibilità (sovrascrivere un metodo protected come public), ma raramente dovresti farlo.
Un esempio completo
Cosa c'è dopo
super è apparso più volte qui — nel concatenamento dei costruttori e come modo per raggiungere un metodo genitore sovrascritto. Il prossimo capitolo sulla parola chiave super illustra ogni situazione in cui appare. Quando un genitore dovrebbe definire cosa fanno i suoi figli ma non come, si ricorre alle classi astratte, che si basano direttamente sulle regole di ereditarietà trattate qui.