Permette di estendere classi esistenti, aggiungendo metodi e componenti.
Differenza fra concetto e implementazione:
Sulla seconda cosa ci torneremo.
Data la classe Studente
class Studente { String nome; int anno; }
Aggiungere la componente int stipendio per studenti borsisti.
class Borsista extends Studente { int stipendio; }
Nomenclatura:
Si dice estendere la classe
In effetti: creare una nuova classe che estende quella esistente.
La classe di partenza non cambia:
Classe | Componenti |
---|---|
Studente | nome, anno |
Borsista | nome, anno, stipendio |
Stesso sistema:
class Borsista extends Studente { int stipendio; int getStipendio() { return this.stipendio; } }
La classe di partenza Studente rimane la stessa (non ha il metodo getStipendio)
La sottoclasse Borsista ha tutti i metodi di Studente più quelli nuovi.
Per gli oggetti delle sottoclassi, usiamo una rappresentazione che evidenzia le parti aggiunte.
Poi vedremo il perchè
Esempio specifico: i borsisti sono una particolare categoria di studenti.
In generale: gli oggetti di una sottoclasse possono avere proprietà che non tutti gli oggetti della classe di partenza hanno.
Usare l'ereditarietà in questi casi.
class Point3D extends Point { int z; }
I punti nello spazio non sono un sottoinsieme dei punti del piano!
Uso metodologicamente sbagliato dell'ereditarietà
Classe B rappresenta un sottoinsieme di A
Tutti gli oggetti di B sono oggetti di A
Gli oggetti di B hanno (almeno) le stesse componenti e gli stessi metodi di quelli di A
Le regole sull'ereditarietà derivano dal concetto di sottoinsieme.
Tutti i Borsisti sono Studenti.
Quindi, uno Studente può anche essere un Borsista.
In una variabile Studente posso mettere il riferimento a un oggetto Borsista:
public static void main(String args[]) { Studente s; Borsista b=new Borsista(); s=b; }
Tecnicamente: da una variabile Studente mi aspetto un oggetto che abbia almeno le componenti nome e anno
Non tutti gli studenti sono borsisti:
public static void main(String args[]) { Studente s=new Studente(); Borsista b; b=s; // errore: incompatible type }
Mettere un riferimento a un Point3D in una variabile Point: si può fare, ma non ha senso.
La classe Point3D va definita da zero, anche se tecnicamente si potrebbe realizzare come sottoclasse di Point:
class Point3D { int x; int y; int z; }
Alcuni studenti sono borsisti.
Si può trasferire il contenuto di una variabile Studente in una variabile Borsista, ma solo se la variabile Studente contiene effettivamente un Borsista.
public static void main(String args[]) { Studente s; Borsista b=new Borsista(); Borsista c; s=b; c=(Borsista) s; }
Si può sempre fare?
Sintatticamente, borsista1=(Borsista) studente1; si può sempre fare.
A run time:
OK:
NO:
Esempio:
public static void main(String args[]) { Studente s=new Studente(); Borsista c; c=(Borsista) s; }
Il compilatore non dà errori.
Quando si esegue:
Exception in thread "main" java.lang.ClassCastException at Runtime.main(Runtime.java:6)
Studente s; if(metodo()) s=new Studente(); else s=new Borsista(); Borsista b=(Borsista) s;
Il contenuto di s dipende dal risultato del metodo...
...che può dipendere dall'input dell'utente, dal contenuto di un file, ecc.
Si può usare una espressione che ritorna un oggetto della sottoclasse in ogni punto del programma dove va messo un oggetto della sovraclasse.
Il contrario si può fare con un cast. Può dare errore a runtime.
Esempio: a un metodo con argomento Studente si può passare new Borsista()
Si può estendere una classe qualsiasi.
Anche una classe che è già il risultato di una estensione:
class Borsista extends Studente { int stipendio; } class BorsistaLaureando extends Borsista { String data_prevista_laurea; }
Tutti i metodi e le componenti di Studente sono in Borsista, e quindi anche in BorsistaLaureando
Una stessa classe si può estendre in più modi:
class StudenteLavoratore extends Studente { String azienza; }
Le classi Borsista e StudenteLavoratore sono entrambe sottoclassi di Studente
Entrambe le classi hanno tutte le componenti e i metodi di Studente
Le componenti aggiuntive di Borsista e StudenteLavoratore possono essere diverse
Uno studente potrebbe essere sia borsista che studente lavoratore.
// errore class BorsistaLavoratore extends Borsista, StudenteLavoratore { ... }
Creare una classe che estende due classi date: ereditarietà multipla.
Non si può fare in Java.
Esistono linguaggio in cui si può fare
In Java, il problema è parzialmente risolto con le interfaccie.
Rappresentazione grafica delle classi definite ora:
La freccia indica una relazione sottoclasse->classe
Esempio di gerarchia che esiste nelle classi predefinite di Java:
Quando possibile, si tende sempre a definire sottoclassi invece di definire classi da zero.
In cima alla gerarchia di tutte le classi c'è la classe predefinita Object:
È una classe predefinita del linguaggio.
Quando si crea una nuova classe senza mettere extends SovraClasse, è implicito extends Object
class Studente { ... } |
= |
class Studente extends Object { ... } |
Ogni classe è sottoclasse di Object (direttamente o indirettamente):
Ogni classe ha un metodo equals e toString
Tutte le classi sono sottoclassi di Object
Tutte le classi hanno i metodi e componenti di Object, che sono:
Tutte le classi hanno equals, toString ecc.
L'indirizzo di un oggetto si può sempre mettere in una variabile di una... sovraclasse? sottoclasse?
Regola facile:
tutti gli oggetti si possono memorizzare in una variabile Object
Ricordare poi che Object è sovraclasse di tutte le altre.
Il controllo di esistenza di componenti e metodi viene fatto in fase di compilazione.
Si assume quindi che in una variabile Classe x ci sia un oggetto di tipo Classe:
public static void main(String args[]) { Object o; Point p=new Point(); o=p; System.out.println(o.x); // errore! }
L'errore è che Object non ha la componente x.
In fase di compilazione, si sa solo che o è un Object
In alcuni casi, cosa c'è nella variabile si può sapere con certezza solo quando si esegue il programma:
public static void main(String args[]) { Object o; if(metodo()) o=new Object(); else o=new Point(); System.out.println(o.x); }
if(metodo()) o=new Object(); else o=new Point();
Il valore di ritorno di metodo() può dipendere dai dati di input, da valori su file, ecc.
Se metodo() ritorna sempre false, allora o.x esiste sempre.
Decidere se un metodo ritorna sempre false è indecidibile
Non esiste nessuna procedura algoritmica per fare questo controllo.
Variabili non inizializzate:
public static void main(String args[]) { int x=10; int y; int z; y=x; if((x==10)||(y!=10)) z=20; System.out.println(z); }
Errore: z potrebbe non essere stata inizializzata.
In questo caso, lo è sicuramente!
In generale, non c'è modo per sapere se una certa istruzione verrà sempre eseguita.
Il compilatore non ci prova nemmeno
Il compilatore usa solo alcune regole semplici, es. si esegue sempre o l'if oppure l'else:
public static void main(String args[]) { int x=10; int y; int z; y=x; if(x==10) z=20; else z=40; System.out.println(z); // ok }
Se un metodo ha un parametro o un valore di ritorno, lo mantiene anche quando il metodo viene ereditato.
Lo stesso vale se il parametro o il valore di ritorno sono del tipo della classe:
class Studente { Studente cambioCanale(Studente s) { ... } } class Borsista extends Studente { }
Nella classe Borsista il metodo cambioCanale ha sempre come valore di ritorno uno Studente (non un Borsista!)
classe | metodo |
---|---|
Studente | Studente cambioCanale(Studente s) |
Borsista | Studente cambioCanale(Studente s) |
Il metodo cambioCanale della classe Borsista ha come parametro e valore di ritorno uno Studente, non un Borsista
I costruttori non si ereditano.
Si può vedere cosí:
I costruttori delle sovraclassi si possono però riusare (poi vediamo come)