W3docs

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 Cat

Animal è 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 public e protected da 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 un Cat include comunque eventuali campi private di Animal), ma la sottoclasse non può vederli per nome. L'unico modo per leggerli è tramite i metodi accessori public/protected ereditati.

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 inheritance

Puoi 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 Money

Questa è 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 runtime

Questa è 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 Stack non è davvero un Vector — eppure java.util.Stack estende Vector ed è 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

java— editable, runs on the server

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.

Esercitazione

Pratica
Quale affermazione sull'ereditarietà in Java è vera?
Quale affermazione sull'ereditarietà in Java è vera?
Was this page helpful?