![]() ![]() |
|
![]() ![]() |
Una classe definisce struttura e funzionalità di una collezione di oggetti. In molti casi occorre definire una classe i cui oggetti
hanno una struttura più ricca di quella di una classe già
definita, oppure che realizzano delle funzionalità aggiuntive.
Nel paradigma di programmazione orientato
ad oggetti, l'ereditarietà
è un meccanismo fondamentale sia per il riutilizzo del codice che
per lo sviluppo incrementale di programmi. Questo meccanismo permette infatti
di estendere e potenziare classi già esistenti,
senza ridefinire variabili
e metodi d'istanza già presenti.
La sintassi per estendere una (super)classe ereditandone struttura e funzionalità è:
|
![]() ![]() |
|
![]() ![]() |
public class Persona { String nome; String indirizzo; public Persona() { this("John Doe","ignoto"); } public Persona(String nome) { this(nome,"ignoto"); } public Persona(String nome, String indirizzo) { this.nome = nome; this.indirizzo = indirizzo; } public String getNome() { return nome; } public String getIndirizzo() { return indirizzo; } public void visualizza() { System.out.println("Nome: " + nome + "\nIndirizzo: " + indirizzo); } public boolean omonimo(Persona p) { return this.nome.equalsIgnoreCase(p.nome); } ... } |
Le variabili d'istanza NON sono state dichiarate private: vedremo più avanti perché.
![]() ![]() |
|
![]() ![]() |
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.
Uno Studente è un tipo particolare di Persona. L'ereditarietà ci consente di definire questa classe senza ripetere la descrizione di tutte le variabili e i metodi di Persona, ma procedendo invece in modo incrementale:
La parola chiave extends significa che
|
![]() ![]() |
|
![]() ![]() |
Un'istanza di Studente avrà quattro variabili di istanza:
Notazione grafica:
![]()
|
![]() ![]() |
|
![]() ![]() |
Concettualmente, possiamo
intrepretare una classe come un insieme, i cui elementi sono
le sue istanze, e la relazione extends come la
relazione di inclusione
tra insiemi. Quindi
|
![]() ![]() |
|
![]() ![]() |
Attenzione: Il compilatore non permette di invocare un metodo di una sottoclasse su di una variabile di una superclasse.
Infatti abbiamo invocato su tizio (variabile dichiarata di classe Persona) un metodo della sottoclasse Studente. In certe situazioni è utile/necessario invocare su di una variabile un metodo di una sottoclasse. In questi casi si può usare l'operazione di cast.
Si ricordi che avevamo già visto il cast per trasformare valori numerici:
Il compilatore consente di effettuare un cast solo verso classi derivate o genitrici (ma in quest'ultimo caso il cast è superfluo).
|
![]() ![]() |
|
![]() ![]() |
Quando si valuta ((Studente) tizio) se tizio non si riferisce ad un'istanza di Studente, verrà generato un errore.
Il predicato instanceof permette di controllare la classe di appartenenza di un oggetto prima del cast:
La condizione restituisce true se e solo se <un_oggetto> è una istanza della classe <Una_Classe> . |
![]() ![]() |
|
![]() ![]() |
Una sottoclasse può anche modificare
dei metodi della superclasse, ridefinendoli.
Ad esempio, se invochiamo il metodo visualizza() su di un'istanza di Studente, verranno stampati solo i valori delle prime due variabili d'istanza (nome e indirizzo). il metodo visualizza() aggiungendo a Studente il seguente metodo:
A tempo di esecuzione, il comando p.visualizza() invocherà quest'ultimo metodo se in quel momento p è un riferimento a un'istanza di Studente, mentre invocherà il metodo visualizza() della classe Persona se p è un riferimento a un'istanza di Persona. |
![]() ![]() |
|
![]() ![]() |
Il meccanismo che determina quale metodo deve
essere invocato in base alla classe di appartenenza dell'oggetto si chiama
binding (legame). Esiste una distinzione tra:
Nell'ultimo comando, il compilatore non può sapere
se a tmp
sarà associato un'istanza di Persona o di Studente, perché questo dipenderà dal
valore fornito dall'utente.
|
![]() ![]() |
|
![]() ![]() |
Il meccanismo di overriding (sovrascrittura) è
concettualmente molto diverso da quello di overloading (sovraccarico), e non deve essere
confuso con esso.
L'overloading consente di definire in una stessa classe più metodi aventi lo stesso nome, ma che differiscano nella firma, cioè nella sequenza dei tipi dei parametri formali. È il compilatore che determina quale dei metodi verrà invocato, in base al numero e al tipo dei parametri attuali. L'overriding consente di ridefinire un metodo in una sottoclasse: il metodo originale e quello che lo ridefinisce hanno necessariamente la stessa firma, e solo l'interprete, a tempo di esecuzione, determina quale dei due deve essere eseguito. |
![]() ![]() |
|
![]() ![]() |
Se nella classe Persona avessimo dichiarato le variabili d'istanza private, il metodo visualizza() di Studente visto sopra avrebbe causato un errore in compilazione, tentando di accedere alle variabili private nome e indirizzo. Se nella classe Persona avessimo dichiarato le variabili d'istanza public, esse sarebbero state accessibili a tutti, in qualsiasi applicazione che usasse la classe Persona. Senza specificare il modificatore di visibilità, le variabili d'istanza di Persona sono accessibili solo nelle classi dello stesso package (in sistemi Linux, nella stessa directory). Dichiarando le variabili protected, si consentirebbe l'accesso alle variabili d'istanza non solo a tutte le classi del package, ma anche a tutte le sottoclassi della classe in questione, anche se appartenenti ad altri packages. |
![]() ![]() |
|
![]() ![]() |
Come sappiamo, la variabile this fa riferimento, nel
corpo di un metodo o di un costruttore, al parametro
implicito, cioè all'oggetto che lo sta
eseguendo.
La variabile super fa riferimento anch'essa all'istanza che sta eseguendo un metodo o un costruttore, ma costringe l'interprete a vedere l'oggetto come istanza della superclasse. La variabile super viene usata tipicamente per accedere a metodi della superclasse che sono stati sovrascritti nella sottoclasse. Ad esempio, possiamo riscrivere il metodo visualizza() di Studente in modo da riutilizzare il metodo visualizza() di Persona.
Come per this, esiste anche una versione con parametri di super, che permette di invocare un costruttore della superclasse. Ad esempio, possiamo riscrivere il costruttore per Studente in modo da richiamare un costruttore per Persona al suo interno.
Attenzione: come per this(), l'invocazione di super() deve essere la prima istruzione del costruttore! |
![]() ![]() |
versione finale |
![]() ![]() |
OSSERVAZIONE: In questa nuova versione, né il costruttore né il metodo visualizza() accedono direttamente alle variabili di istanza nome e indirizzo ereditate dalla superclasse. Nella superclasse Persona potremmo quindi dichiarare queste variabili private (che è spesso la scelta migliore).
public class Studente extends Persona { // Studente eredita variabili e metodi da Persona int matricola; // Nuova variabile istanza String pianoDiStudio; // Nuova variabile istanza static int nextMatricola = 1; // Nuova variabile statica (di classe) // Costruttore public Studente(String nome, String indirizzo) { super(nome,indirizzo); this.matricola = nextMatricola++; this.pianoDiStudio = ""; } // Nuovo metodo public String getPdS() { return pianoDiStudio; } // Nuovo metodo public void modificaPdS(String nuovoPdS) { pianoDiStudio += nuovoPdS + "\n"; } // Metodo sovrascritto public void visualizza() { super.visualizza(); System.out.println("Matricola: " + matricola + "\nPiano di Studio: " + pianoDiStudio); } ... } |
![]() ![]() |
la classe Professore |
![]() ![]() |
Vogliamo definire una classe Professore
, che rappresenti i docenti di un corso di laurea. Ogni professore
è descritto dal nome, dall'indirizzo, dal ruolo, dallo stipendio
e dai corsi affidati.
Un Professore è un tipo particolare di Persona.
|