Ereditarieta’
Contenuti
• Introduciamo un meccanismo fondamentale di
Java: l’ereditarieta’
• Permette di estendere classi gia’ definite (ovvero
di definire sottotipi di tipi gia’ definiti)
• Introduciamo i meccanismi principali (l’utilizzo
dell’ereditarieta’ lo riprenderemo in seguito)
Ricordiamo che
I programmi in Java consistono di classi.
Le classi consentono di definire:
• collezioni di procedure (metodi statici)
• come prototipi di oggetti (tramite variabili e
metodi d’istanza, che definiscono il loro
stato interno e le operazioni)
Ricordiamo che……
Variabili e metodi statici appartengono alla classe
Variabili e metodi di istanza appartengono agli
oggetti (istanze della classe).
Una classe definisce un tipo (nome della classe).
Gli oggetti istanza della classe hanno quel tipo.
Ereditarietà
L’ ereditarieta’ è un meccanismo fondamentale
sia per il riutilizzo del codice che per lo sviluppo
incrementale di programmi.
Questo meccanismo permette di
• estendere e potenziare classi già esistenti
•fattorizzare informazioni comuni a piu’ classi
A cosa serve
Supponiamo di volere definire
• una classe i cui oggetti hanno una struttura più ricca di
quella di una classe già definita
•una classe che realizza delle funzionalità aggiuntive
rispetto ad una classe già definita
In questi casi si può definire la nuova classe come
sottoclasse della precedente, ereditando le caratteristiche
gia’ presenti.
Esempio
Supponiamo di avere definito una classe Persona
• gli oggetti della classe memorizzano il nome e l’indirizzo
• semplici operazioni definite tramite metodi d’istanza
Esempio di classe 1
public class Persona
{
public String nome;
public String indirizzo;
public Persona()
{this.nome = "";
this.indirizzo = ""; }
public Persona(String nome,String indirizzo)
{this.nome = nome;
this.indirizzo = indirizzo; }
public String getNome()
{return nome;
}
Esempio di classe 2
public String getIndirizzo()
{return indirizzo; }
public void visualizza()
{System.out.println("Nome: " + nome +
"\nIndirizzo: " + indirizzo); }
public boolean omonimo(Persona p)
{return this.nome.equals(p.nome);
}
}
Esempio di sottoclasse 1
•Vogliamo definire una classe Studente che rappresenti
gli studenti iscritti ad un corso di laurea.
•Ogni studente è descritto dal nome, dall'indirizzo,
dal numero di matricola e dal piano di studio.
•Vogliamo delle operazioni addizionali per leggere e
modificare il piano di studio
•Uno Studente è un tipo particolare di Persona.
Esempio di sottoclasse 1
•L'ereditarietà ci consente di definire la classe Studente
senza ripetere la descrizione di tutte le variabili e i metodi
di Persona
•Definiamo Studente in modo incrementale,
come sottoclasse
Esempio di sottoclasse 2
public class Studente extends Persona
{ public int matricola;
public String pianoDiStudio;
public static int nextMatricola = 1;
public Studente()
{ this.matricola = nextMatricola;
nextMatricola=nextMatricola+1;
this.pianoDiStudio = "";
}
public Studente(String nome, String indirizzo)
{this.nome = nome;
this.indirizzo = indirizzo;
this.matricola = nextMatricola;
nextMatricola= nextMatricola+1;
this.pianoDiStudio = "";
}
Esempio di sottoclasse 3
public String getPdS()
{return pianoDiStudio;
}
public void modificaPdS(String nuovoPdS)
{ pianoDiStudio += nuovoPdS + "\n";
}
}
La parola chiave
extends
significa che
è una sottoclasse o classe derivata di Persona
Persona è una superclasse o classe genitrice di Studente
Studente
Analogamente
Studente
è un sottotipo di Persona
Semantica informale
Se c1 è una sottoclasse di (estende) c2
• le variabili e metodi statici di c2 (e delle sue
superclassi) sono visibili direttamente da c1
•variabili e metodi di istanza di c2 (e delle sue
superclassi) diventano anche variabili e metodi di
istanza di c1 (a meno di overriding)
•Un oggetto di tipo Studente avrà quattro variabili di
istanza:
*nome e indirizzo ereditate da Persona
*matricola e pianoDiStudio definite nella
sottoclasse Studente
•inoltre avra’ tutti i metodi d’istanza della sottoclasse e
della superclasse (a meno di sovrascrittura)
•per quanto riguarda le variabili ed i metodi statici dichiarati
nella superclasse (non ce sono..) possono essere usati come se
fosse un oggetto di tipo Persona
Un oggetto di tipo Studente
Esempio
Studente p= new Studente();
String s1= p.getIndirizzo(); (metodo della superclasse)
String s2=p.getPds(); (metodo della sottoclasse)
String s3=p.nome; (variabile della superclasse)
int x=p.matricola; (variabile della sottoclasse)
•Le variabili sono pubbliche (non ci sono vincoli per l’accesso)
Costruttori
Anche per i costruttori esiste un meccanismo di ereditarietà:
•all’atto della creazione di un oggetto della sottoclasse
c1 viene eseguito automaticamente il costruttore della
superclasse c2 (per inizializzare le variabili ereditate)
Esempio: costruttori
public Studente()
{ this.matricola = nextMatricola ++;
this.pianoDiStudio = ""; }
public Persona() {this.nome = "";
this.indirizzo = ""; }
•quando viene eseguito new Studente() il costruttore di Persona viene invocato
automaticamente per inizializzare le variabili eredidate alla stringa vuota
•possibile per i costruttori di default (che non hanno parametri)
Altrimenti…
public Studente(String nome, String indirizzo)
{this.nome = nome;
this.indirizzo = indirizzo;
this.matricola = nextMatricola;
nextMatricola= nextMatricola+1;
this.pianoDiStudio = "";
}
•Le variabili ereditate vanno inizializzate esplicitamente assegnando i
valori dei parametri
Overriding
•In alcuni casi i metodi ereditati dalla superclasse
possono non essere adatti per la sottoclasse
•Una sottoclasse puo’ sovrascrivere (dichiarare) un
metodo della superclasse (stesso nome, stessi
parametri, stesso tipo)
•In tal caso sugli oggetti della sottoclasse viene
utilizzato il metodo riscritto (quello piu’ specifico)
Ad esempio, se invochiamo il metodo visualizza su un'istanza di
Studente, verranno stampati solo i valori delle prime due variabili
d'istanza (nome e indirizzo).
public void visualizza()
{System.out.println("Nome: " + nome +
"\nIndirizzo: " + indirizzo);
}
Se vogliamo stampare anche la matricola ed il piano di studio,
dobbiamo sovrascrivere (override) visualizza aggiungendo a
Studente la dichiarazione del seguente metodo:
public void visualizza()
{System.out.println("Nome: " + nome + "\nIndirizzo: " +
indirizzo);
System.out.println("Matricola: " + matricola +
"\nPianodiStudio: " + pianoDiStudio);
}
Il comando
p.visualizza()
invocherà
•il metodo visualizza della classe Persona se p è un'istanza di
Persona
•il nuovo metodo che stampa anche il numero di matricola e il
piano di studio se p è un'istanza di Studente.
•La scelta del metodo piu’ specifico viene effettuata a tempo di
esecuzione in base al tipo di p
•Non puo’ essere fatta a tempo di compilazione (come vedremo
a causa dei sottotipi)
Specificatori di accesso : private e
public
Ricordiamo che le variabili private sono visibili solo nella
classe in cui sono dichiarate
Se nella classe Persona avessimo dichiarato le variabili
d'istanza private, non sarebbe stato possibile accedere dalla
sottoclasse alle variabili private dichiarate nella superclasse.
Variabili della superclasse private
•Gli oggetti di tipo Studente possiedono le variabili della
superclasse (le ereditano)
•Ma non possono accedervi!
•Il codice della sottoclasse non va bene: problemi nei
costruttori e nel metodo visualizza() (sovrascritto)
Problema I
Il metodo visualizza di Studente
avrebbe causato un errore in compilazione, tentando di
accedere a variabili private dichiarate nella superclasse.
public void visualizza()
{System.out.println("Nome: " + nome +
"\nIndirizzo: " + indirizzo);
System.out.println("Matricola: " + matricola +
"\nPianodiStudio: " +
pianoDiStudio);
}
Problema II
public Studente(String nome, String indirizzo)
{this.nome = nome;
this.indirizzo = indirizzo;
this.matricola = nextMatricola;
nextMatricola= nextMatricola+1;
this.pianoDiStudio = "";
}
Super
Allora come avrebbe fatto un oggetto di tipo Studente a
modificare le variabili d’istanza ereditate dalla superclasse?
Si puo’ accedere alle variabili attraverso i metodi ed i costruttori
della superclasse utilizzando super
(simile a this) fa riferimento all'istanza che sta
eseguendo un metodo o un costruttore, ma costringe
l'interprete a vedere l'oggetto come istanza della
superclasse.
super
Esempio 1
Riscriviamo il metodo visualizza per Studente
in modo da chiamare il metodo visualizza di Persona
per accedere alle variabili della superclasse
public void visualizza()
{ super.visualizza();
System.out.println("Matricola: " + matricola +
"\nPianodiStudio: " +
pianoDiStudio); }
Il metodo fa la stessa cosa di prima, ma lo fa in modo diverso!
Esempio 2
Riscriviamo il costruttore in modo da chiamare
il costruttore superclasse
public Studente(String nome, String indirizzo)
{super(nome,indirizzo);
this.matricola = nextMatricola;
nextMatricola= nextMatricola+1;
this.pianoDiStudio = "";
}
Gerarchia di Tipi
• La relazione di sottoclasse e’ transitiva
• Si crea una gerarchia di classi (o di tipi) al cui top
c’e’ la classe primitiva Object
• Tutti i tipi che definiamo sono per default sottotipi
di Object
Principio di sostituzione
Grazie all’ereditarieta’ i sottotipi supportono il comportamento
del supertipo, ovvero le istanze della sottoclasse
• hanno le variabili d’istanza ed i metodi (al limite overridden)
del supertipo
•hanno accesso alle variabili ed ai metodi statici della superclasse
Di conseguenza un oggetto del sottotipo può essere utilizzato
dovunque sia richiesto un oggetto del supertipo.
Esempio 1
Un'istanza di Studente si può usare in qualsiasi espressione,
dove sia richiesto un oggetto di Persona, come in un assegnamento
o nel passaggio dei parametri.
Persona tizio = new Studente("Mario Rossi", "Pisa");
/* corretto: su tizio posso invocare tutti i metodi di
Persona, grazie all'ereditarieta' */
tizio.visualizza() ;
tizio.nome=“Francesca”;
Esempio
Un'istanza di Studente si può usare dovunque sia richiesto un
oggetto di Persona, come in un assegnamento o nel passaggio
di parametri.
Persona tizio = new Persona("Marco Rossi", "Pisa");
Studente pippo = new Studente("Mario Rossi", "Pisa");
/* corretto: omonimo richiede un parametro di tipo
Persona */
...
if(tizio.omonimo(pippo))
...
Attenzione
•Non è possibile il contrario, ovvero utilizzare un oggetto del
supertipo al posto di uno del sottotipo.
•Il sottotipo puo’ avere variabili e metodi (d’istanza o statici)
aggiuntivi, per esempio il numero di matricola o il metodo che
ritorna il piano di studio.
...
Esempio
...
Studente tizio = new Persona("Mario Rossi", "Pisa");
String s=tizio.getpdS();
• L’oggetto
associato alla variabile tizio e’ di tipo Persona,
non possiede il metodo getPdS()
•Per evitare errori a run-time, questo e’ un errore di tipo
rilevato staticamente dal compilatore!!!!
Flessibilita’
•Il principio di sostituzione e’ fondamentale per
sfruttare i sottotipi
•Permette di riusare tutto il codice definito per il
supertipo per tutti i sottotipi
•Permette di realizzare una sola volta operazioni
comuni tra vari sottotipi
•Nel seguito vedremo di capire meglio perche’, per
ora vediamo un esempio
public boolean omonimo(Persona p)
{return this.nome.equals(p.nome);
}
omonimo richiede un parametro di tipo Persona, grazie al
principio di sostituzione lo possiamo usare anche
per il sottotipo (altrimenti avremmo dovuto ridefinirlo)
Studente tizio = new Studente("Marco Rossi", "Pisa");
Studente pippo = new Studente("Mario Rossi", "Pisa");
if(tizio.omonimo(pippo))
Tipo apparente e tipo effettivo
•Il principio di sostituzione crea pero’ dei problemi
per i controlli statici dovuti alla presenza dei sottotipi
•Tipo apparente : tipo con cui una variabile e’ dichiarata
•Tipo effettivo: tipo del valore legato alla variabile
Il tipo apparente ed il tipo effettivo possono essere
diversi,
in particolare il tipo effettivo puo’ essere un sottotipo del tipo
apparente
Esempio
Persona tizio = new Studente("Mario Rossi", "Pisa”);
tizio
ha
•tipo apparente Persona
•tipo effettivo Studente
...
Il compilatore?
• Il controllo dei tipi in Java e’ effettuato staticamente dal
compilatore (non a run-time)
• Il compilatore di Java effettua una analisi statica, ovvero
una verifica sul codice del programma, che esamina tutte le
istruzioni separatamente e controlla che ogni assegnamento
e ogni chiamata di metodo (passaggio di parametri) sia
corretta rispetto ai tipi delle variabili
Tipo Apparente
• Come fa il compilatore a usare il tipo effettivo di
una variabile senza eseguire un programma?
• Non puo’ (dipende dal flusso di esecuzione)
calcolare il tipo del valore associato ad una variabile
• Di consequenza il controllo dei tipi fatto dal
compilatore usa solo il tipo apparente
Attenzione
Persona tizio = new Studente("Mario Rossi",
"Pisa");
tizio.modificaPdS("Algebra");
Si verifica un errore di compilazione. Infatti abbiamo chiamato
su tizio (variabile dichiarata di classe Persona) un metodo della
sottoclasse Studente.
Anche se il tipo effettivo sarebbe giusto……il compilatore
non lo conosce!
E’ possibile che staticamente si rilevino errori di tipo che
non sarebbero tali a run-time (ovvio)
Il Cast: spostare il controllo a run-time
((Sottotipo)
espressione)
• il compilatore tratta espressione come se fosse del
Sottotipo (si fida)
•Il controllo che il tipo effettivo di espressione sia
Sottotipo e’ rimandato a run-time (errore a
tempo di esecuzione)
Esempio di Cast
Persona tizio = new Studente("Mario Rossi",
"Pisa");
((Studente)tizio).modificaPdS("Algebra");
• non
ci sono errori in fase di compilazione, il compilatore
verifica solo che il tipo apparente di tizio sia supertipo di
Studente (tipo verso il quale facciamo il cast)
•tratta nella chiamata di metodo tizio come se fosse del
sottotipo
A run-time
Quando si valuta ((Studente) tizio) se tizio non ha tipo effettivo
Studente, verrà invece sollevata una eccezione a run-time
(ClassCast)
Persona tizio = new Studente("Mario Rossi","Pisa");
((Studente)tizio).modificaPdS("Algebra");
• non
ci sono errori in fase di esecuzione in questo caso!!!
•il cast e’ molto utile, come vedremo indispensabile per programmare
Controllo del tipo di un oggetto
Si può controllare la classe di appartenenza di un oggetto prima
del cast:
if (tizio instanceof Studente)
((Studente) tizio).modificaPdS("Algebra");
La condizione (obj instanceof Classe) restituisce
true se e solo se obj è una istanza della classe Classe .
Tipo effettivo e
apparente:conseguenza
• Il controllo dei tipi statico non evita errori di
tipo a run-time a causa dei cast
• La scelta del metodo piu’ specifico tra
metodi overridden dipende dal tipo effettivo
(deve essere fatta a run-time)
Esempio
• Supponiamo di avere riscritto il metodo visualizza
nella sottoclasse studente
Persona tizio = new Studente("Mario Rossi", "Pisa");
tizio.visualizza();
•Compila correttamente
•Deve invocare il metodo piu’ specifico
(quello della sottoclasse) che dipende
dal tipo effettivo
Visibilita’ dei nomi e regole di scoping
• Ricordiamo che l’analisi statica effettuata dal compilatore
verifica anche le regole di scoping
•Anche queste vengono realizzate rispetto al tipo apparente
•Le regole sono quelle che abbiamo visto
•L’estensione ai sottotipi e’ banale
Variabili + metodi statici
• Da una classe sono direttamente visibili nell’ordine
i propri nomi statici e quelli delle superclassi
•I metodi statici hanno la visibilita’ della classe di
appartenenza (quella in cui sono dichiarati) oltre
ai nomi locali
Non vediamo i nomi d’istanza, infatti non ci sono oggetti di
riferimento
Variabili + metodi d’istanza
• Da un oggetto sono direttamente visibili nell’ordine
i propri nomi d’istanza (inclusi quelli ereditati) e la classe,
tramite questa le variabili statiche sue e delle superclassi
•I metodi d’istanza (inclusi i costruttori) hanno la visibilita’
dell’oggetto su cui sono eseguiti
•Infatti le variabili d’istanza appartengono agli oggetti, e le
variabili statiche sono condivise tra tutti gli oggetti della classe
Scarica

Lucidi