Riusare classi | Inizializzare la classe base | Composizione di classi e ereditarietà | Composizione di classi vs ereditarietà
Un modo per riutilizzare del codice già definito consiste nell'introdurre
una nuova classe come un tipo di una classe esistente. Tramite il meccanismo
dell'ereditarietà si aggiunge del codice che specializza la classe di partenza.
Per definire una classe che ne specializzi una di base si deve usare la
parola riservata extends nome-classe-base
.
La classe base
Disinfettante
, viene estesa
dalla classe Detergente
:
class Disinfettante { private String s = new String ("disinfettante"); public void appendi (String arg) { s += arg; } public void diluisci () { appendi (" diluisci ()"); } public void applica () { appendi (" applica ()"); } public void gratta () { appendi (" gratta ()"); } public void print () { System.out.println (s); } public static void main (String [] args) { Disinfettante dis = new Disinfettante (); dis.diluisci(); dis.applica(); dis.gratta(); dis.print(); } }
public class Detergente extends Disinfettante { // sovrascrive il metodo gratta () public void gratta () { appendi ( " Detergente.gratta()"); super.gratta (); // chiamata al metodo della superclasse } // aggiunge nuovi metodi all'interfaccia public void schiuma () { appendi (" schiuma()"); } public static void main (String [] arg) { Detergente det = new Detergente (); det.diluisci (); det.applica(); det.gratta(); det.schiuma(); det.print (); System.out.println ("Test della classe base:"); Disinfettante.main (arg); } }
In questo esempio si nota che entrambe le classi definiscono un metodo main
.
È possibile che ogni classe definisca un metodo main
per facilitare
il compito di verificare ciascuna delle classi durante la loro definizione
o nei successivi cambiamenti. La cosa è possibile purché ci sia solo una classe
dichiarata con accesso public
: solo il metodo main
di questa classe verrà automaticamente chiamato. Gli altri, come nell'esempio,
potranno essere invocati nel codice direttamente. In Detergente.main()
si
invoca esplicitamente il metodo main()
della superclasse, con l'istruzione
Disinfettante.main()
. Non è necessario eliminare i metodi main
delle diverse classi, una volta effettuato il test; si possono lasciare per semplificare il compito
in caso di ulteriori cambiamenti successivi.
Nella classe Detergente
, i metodi appendi()
, diluisci()
,
applica()
vengono ereditati dalla superclasse Disinfettante
e possono essere usati.
L'ereditarietà può essere vista
come un modo per riusare l'interfaccia. Il metodo gratta()
, invece,
viene ridefinito o sovrascritto: e per poter invocare il vecchio metodo della superclasse,
occorre esplicitamente chiamarlo, usando il riferimento super
, che si riferisce
alla superclasse della classe corrente. Cioè super.gratta()
chiama
il metodo gratta()
della classe base.
Possiamo poi aggiungere metodi all'interfaccia definendo metodi esclusivamente per la sottoclasse.
In questo caso il metodo schiuma()
sarà definito solo per la classe
Detergente
e non per la classe base Disinfettante
.
Mediante il meccanismo di ereditarietà si possono introdurre nuove classi da classi pre-esistenti. La nuova classe ha la stessa interfaccia della vecchia e probabilmente qualche nuovo metodo aggiuntivo. Un oggetto della nuova classe contiene quindi anche un oggetto della classe di base da cui deriva. Questo sotto-oggetto è come quello che avremmo creato nella classe base e ha vita nel contesto del nuovo oggetto.
Schizzo
specializza la classepublic class Schizzo extends Disegno { Schizzo () { System.out.println ("costruisco un oggetto Schizzo"); } public static void main (String [] arg) { Schizzo x = new Schizzo(); } }
Disegno
,
che a sua volta è una sottoclasse della classeclass Disegno extends Arte { Disegno () { System.out.println ("costruisco un oggetto Disegno"); } }
Arte
class Arte { Arte () { System.out.println ("costruisco un oggetto Arte"); } }
Il costruttore della classe Schizzo
nell'istanziare un oggetto di tipo Schizzo
richiama il costruttore della
superclasse Disegno
, che a sua volta richiama il costruttore della
sua superclasse Arte
. Infatti la chiamata al costruttore Schizzo()
causa la stampa dei tre messaggi:
costruisco un oggetto Arte costruisco un oggetto Disegno costruisco un oggetto Schizzo
dove si noti l'ordine con cui sono stati istanziati gli oggetti a partire da quello
più alto in gerarchia.
I costruttori della superclasse vengono chiamati
automaticamente; questo non è però possibile nel caso in cui i costruttori
richiedano parametri: in questo caso è necessario invocare esplicitamente il
costruttore della superclasse e passare i parametri necessari con l'istruzione
super(lista_parametri)
.
È molto comune usare sia la combinazioni di classi sia l'ereditarietà per creare strutture di classi complesse.
class Piatto { Piatto (int i) { System.out.println ("Costruttore di " +i+ " piatti"); } } class Piatto_cena extends Piatto { Piatto_cena (int i) { super (i); System.out.println ("Costruttore di " +i+ " piatti per posto"); } } class Posata { Posata(int i) { System.out.println ("Costruttore di " +i+ " posate"); } } class Cucchiaio extends Posata { Cucchiaio (int i) { super (i); System.out.println ("Costruttore di " +i+ " cucchiai"); } } class Forchetta extends Posata { Forchetta (int i) { super (i); System.out.println ("Costruttore di " +i+ " forchette"); } } class Coltello extends Posata { Coltello (int i) { super (i); System.out.println ("Costruttore di " +i+ " coltelli"); } } class Posto_a_tavola { Posto_a_tavola (int i) { System.out.println ("Costruttore di posti a tavola di base: " +i); } } public class Tavola extends Posto_a_tavola { Cucchiaio cucch; Forchetta forch; Coltello colt; Piatto_cena piatti; Tavola(int i) { super (i+1); cucch = new Cucchiaio (i+1); forch = new Forchetta ((i+1)*2); colt = new Coltello (i+1); piatti = new Piatto_cena ((i+1)*3); System.out.println ("Costruttore di una tavola apparecchiata per " + (i+1) + " posti"); } public static void main (String [] arg) { Tavola x = new Tavola (2); } }
La gerarchia tra le classi del programma è mostrata nella figura dove è messo in risulta sia la copmposizione tra le classi (la classe Tavola contiene sotto-oggetti delle classi Piatto_cena, Cucchiao, Forchetta e Coltello) sia la gerarchia determinata dalla ereditarietà.
Il programma Tavola.java è tutto contenuto in un file, per questo è necessario
che solo una classe sia dichiarata public
: la classe Tavola
che estende
la classe Posto_a_tavola
. La compilazione del programma produce tanti file bytecode quante sono
le classi contenute nel singolo file e dà luogo al seguente output:
Costruttore di posti a tavola: 3 Costruttore di 3 posate Costruttore di 3 cucchiai Costruttore di 6 posate Costruttore di 6 forchette Costruttore di 3 posate Costruttore di 3 coltelli Costruttore di 9 piatti Costruttore di 9 piatti per cena Costruttore di una tavola apparecchiata per 3 posti
Il compilatore costringe a inizializzare la classe base e all'inizio del costruttore, ma non controlla che vengano inizializzati i membri della classe. Per questo è opportuno che le inizializzazione vengano effettuate nei costruttori.
Sia la composizione tra classi sia l'ereditarietà consentono di creare sotto-oggetti
in una classe. La composizione viene di solito usata quando si vogliono le caratteristiche
di una classe esistente ma non necessariamente la sua interfaccia. Il sotto-oggetto in questo caso
viene immerso nel nuovo in modo da poterlo usare per implementare le nuove caratteristiche
della nuova classe, ma l'utente vedrà solo l'interfaccia della nuova classe e
non quella del sotto-oggetto immerso. Per questo effetto i sotto-oggetti
sono definiti private
nella nuova classe.
Altre volte è utile lasciare che l'utente della nuova classe possa direttamente
accedere al sotto-oggetto: in questo caso essi vanno definiti public
. Come
nell'esempio Automobile.java, in cui i membri public
dei sotto-oggetti possono essere utilizzati direttamente:
class Motore { public void accendi () { } public void indietro () { } public void spegni () { } } class Pneumatico { public void gonfia ( int psi ) { } } class Finestrino { public void su () { } public void giu () { } } class Porta { public Finestrino finestrino = new Finestrino(); public void apri () { } public void chiudi () { } } public class Automobile { public Motore motore = new Motore(); public Pneumatico [] pneumatici = new Pneumatico [4]; public Porta porta_sn = new Porta (); public Porta porta_dx = new Porta (); Automobile () { for (int i = 0; i < 4; i++) pneumatici[i] = new Pneumatico(); } public static void main (String [] arg) { Automobile auto = new Automobile (); auto.pneumatici[0].gonfia(72); auto.porta_sn.chiudi(); auto.motore.accendi(); auto.porta_sn.finestrino.su (); auto.motore.indietro(); } }
Nell'ereditarietà invece si prende una classe esistente e la si specializza per uno scopo particolare. Dipende dal problema scegliere se specializzare le classi o comporle. Nel caso dell'esempio dell'automobile ha senso comporre le classi perchè un'automobile è composta da varie componenti. La composizione esprime la relazione has-a mentre l'ereditarietà, esprime la relazione is-a.