Ereditarietà
Quando si scrive un programma con un linguaggio orientato agli oggetti bisogna individuare le classi che occorre sviluppare e aggregarle per permettere l’interazione tra gli oggetti corrispondenti.
Una volta individuata una classe, accade spesso di dover definire un’altra classe con caratteristiche simili a quella già definita.
In questi casi conviene soltanto specificare le differenze, cioè definire soltanto le proprietà e i comportamenti diversi, supponendo che tutti gli altri attributi e metodi siano quelli già definiti nella prima classe. In Java è possibile fare questo grazie al concetto di ereditarietà.
L’ereditarietà è un meccanismo che, in fase di definizione di una classe, permette di specificare solo le differenze rispetto ad una classe già esistente, detta superclasse. Tutte le altre caratteristiche e i comportamenti della classe che si sta definendo saranno gli stessi della superclasse.
Riprendiamo l’esempio della classe rettangolo R supponendo di voler gestire anche eventuali quadrati: il quadrato può essere, infatti, visto come un caso particolare di rettangolo (una specializzazione di rettangolo). Possiamo, allora, concepire una classe Quadrato Q mantenga le caratteristiche comuni con la classe R (area,perimetro) ma che si differenzia per il fatto che ha un singolo parametro (lato) invece che due (base, altezza).
Secondo la notazione UML per rappresentare questo legame gerarchico tra le due classsi R e Q si usa la seguente simbologia:
R è superclasse
di Q
Q è sottoclasse
di R
R è la classe padre
Q è la classe figlio.
La classe figlio, dalla classe padre, eredita tutto: attributi e metodi, in modo particolare i comportamenti comuni, come, in questo caso, i metodi per il calcolo dell'area e del perimetro.
La classe figlio avrà, d'altro canto, delle peculiarità che sono di sua esclusiva proprietà come il valore del lato ed un modo diverso di esternalizzare le proprie caratteristiche tramite il metodo toString(). Il metodo toString() sarà dunque riscritto e adattato per l'oggetto quadrato Q. Questo processo di ridefinizione di eventuali metodi già presenti nella classe padre nella classe figlio è noto con l'appellativo di overriding (sovrascrittura).
L'overriding consiste nel riscrivere in una sottoclasse un metodo della superclasse con la stessa segnatura (cioè con lo stesso nome e gli stessi parametri di ingresso). Più in generale si possono avere metodi con lo stesso prototipo in classi differenti.
La nuova classe può differenziarsi dalla superclasse o per ridefinizione (overriding) o per estensione, quando, semplicemente si aggiungono nuovi metodi o attributi non presenti nella superclasse.
class GEO {
public static void main (String args []) {
R r = new R(3, 2);
System.out.println(r.getb());
System.out.println(r);
Q q=new Q(6);
System.out.println(q);
}//fine main
}// fine classe
class R {
private int b,h,a,p;
private int area(int b,int h){return b*h;}
private int peri(int b,int h){return 2*b+2*h;}
R(int x, int y) {//costruttore
b=x;h=y;
a=area(b,h);p=peri(b,h);
}
public int getb() { return b;}
public int geth() { return h;}
public int geta() { return a;}
public int getp() { return p;}
public String toString() {
String st="base:"+getb()+" altezza:"+geth();
st+=" area:"+geta()+" perimetro:"+getp();
return st;
}//fine toString()
}//fine classe R
class Q extends R{
private int l;
Q(int lato){
super(lato,lato);
l=lato;
}
public int getl() { return l;}
public String toString() {
String st="lato:"+getl();
st+=" area:"+geta()+" perimetro:"+getp();
return st;
}//fine toString()
}//fine classe Q
In sintesi, la sottoclasse
● eredita attributi e metodi dalla superclasse
(lo stato interno e il comportamento) e può:
● estendere la superclasse aggiungendo nuovi
metodi e attributi.
● ridefinire attributi e metodi (overriding
o sovrascrittura).
Nell'esempio riportato la ridefinizione del metodo toString() è evidente. L'estensione della classe Q dalla classe R è assicurata dalla forma class Q extends R.
extends significa che la classe Q è ottenuta dalla classe R estendendone lo stato e il comportamento.
Questo non implica che tutti i membri della classe rettangolo R siano accessibili nel codice della classe quadrato Q, ciò dipende dalla visibilità di tali membri.
Se vogliamo liberamente usare attributi e metodi della superclasse nella sottoclasse sarà necessario utilizzare il modificatore di accesso protected che stabilisce che l'entità è protetta ed è visibile solo nella classe corrente e in tutte le sottoclassi della classe che contiene la definizione.
Nell'esempio che abbiamo fatto non è necessario usare protected,
perché Q utilizza soltanto metodi pubblici di R
( geta() e getp() ) ma se
all'interno del codice di Q si volesse invocare (ad
es.) il metodo area(...) della classe R,
questo sarebbe disponibile solo se fosse dichiarato protected
(e non private).
Riportiamo in fondo alla pagina un elenco dei principali
modificatori di accesso del linguaggio Java.
La prima istruzione del costruttore nella classe derivata è l'invocazione al costruttore della superclasse:
super(lato,lato);
La costruzione di un oggetto di una sottoclasse avviene, dunque, costruendo
un oggetto della superclasse e adattandolo alle esigenze della sottoclasse.
Nel caso di overloading, cioè nel caso
vi siano più costruttori nella superclasse, il costruttore invocato viene
riconosciuto in base alla lista degli argomenti fornita al super.
L'invocazione del costruttore della classe padre, tramite il super deve essere la prima istruzione del costruttore della classe derivata.
Se in una classe non si definiscono costruttori, viene automaticamente aggiunto un costruttore privo di argomenti (e privo di codice).
Se un costruttore non ne invoca esplicitamente un altro tramite il super, come prima istruzione viene automaticamente invocato il costruttore privo di argomenti della superclasse.
Se il costruttore della classe padre si aspetta dei parametri, bisogna farglieli arrivare tramite l'istruzione super, altrimenti si ha un errore di compilazione.
Nel seguente listato la classe padre P non ha un
costruttore; ha due metodi pubblici
getSomma()
setSomma(int importo)
che automaticamente possono essere usati dalla classe figlio F.
L' attributo protected: somma presente nella classe
P può essere usato esclusivamente dalla classe figlio
F.
Il costruttore della classe figlio possiede un parametro che consente di inizializzare l'oggetto f con una somma prestabilita, ma prima di effettuare questa operazione viene stampato a video il valore della somma posseduta dall'oggetto f nel momento della sua creazione. Tale valore è 0 (zero) questo vuol dire che i due oggetti p ed f non trasferiscono somme tra loro, essi hanno una vita autonoma. L'oggetto f non eredita dei valori dall'oggetto p ma solo dei parametri (attributi) e dei comportamenti (metodi).
Nell'implementazione della classe P non è stato
scritto esplicitamente alcun costruttore e in tale classe il compilatore
aggiunge implicitamente
P(){}
mentre nella classe figlio F aggiunge:
F() {
super();
}
Polimorfismo
Col termine polimorfismo si intende la capacità di definire comportamenti diversi a secondo del contesto in cui ci si trova in risposta ai medesimi input esterni.
Un oggetto, denota un aspetto polimorfo quando può rispondere in modo diverso a seconda del contesto in cui si trova. Esistono due tipi di polimorfismo:
1 polimorfismo orizzontale (o per metodi)
2 polimorfismo verticale (o per classi, o per
dati).
Nel caso 1 vengono sfruttati i meccanismi di overriding o di overload di metodi per definire elaborazioni differenti in sottoclassi differenti.
questo approccio può essere illustrato nel seguente esempio:
class suoni {
public static void main(String[] args) {
strumento s = new strumento();
chitarra c = new chitarra();
tamburo t = new tamburo();
s.suona();
c.suona();
t.suona();
}
}
class strumento {
void suona() {
System.out.println("ogni strumento produce un suono");
}
}
class chitarra extends strumento {
void suona() {
System.out.println("dlen-dlen");
}
}
class tamburo extends strumento {
void suona() {
System.out.println("bum-bum");
}
}
Il metodo suona() è stato ridefinito tramite overriding in ciascuna delle sottoclassi della superclasse strumento.
Niente ci avrebbe impedito di dichiarare i due oggetti istanziati come:
strumento c = new chitarra();
strumento t = new tamburo();
ed è questa la forma di polimorfismo 2 : il polimorfismo per classi o per dati; ma un oggetto di una classe può diventare un oggetto di un'altra classe?
tamburo t=new tamburo()
strumento s=new strumento()
s=t
t=s
la prima istruzione è più immediata perché se t è un tamburo è sicuramente uno strumento. Non è invece così scontata la seconda istruzione, infatti se s è uno strumento, potrebbe non essere un tamburo ma una chitarra o un violino.
Lo stesso si sarebbe potuto fare nel programma sul rettangolo e il quadrato modificando il programma nel corpo del main() nel seguente modo:
R r;
Q q=new Q(6);
r=q;
System.out.println(r);
Quale metodo toString() viene eseguito? Ma quello del quadrato, ovviamente. Il metodo da eseguire viene scelto dalla JVM in fase di esecuzione e dipende dal tipo dell'oggetto e non dal tipo del riferimento.
In fase di compilazione si verifica l'esistenza, per il tipo del riferimento
utilizzato, di un metodo che soddisfi la chiamata.
In fase di esecuzione viene selezionato il metodo da eseguire, sulla base
del tipo effettivo dell'oggetto (e non del tipo del riferimento).
La ricerca avviene a partire dalla classe dell'oggetto, risalendo nella
gerarchia.
Poiché il compilatore ha controllato l'esistenza di un tale metodo per il tipo del riferimento, prima o poi il metodo selezionato sarà trovato (al massimo risalendo nella gerarchia fino al tipo del riferimento).
Dicevamo che i meccanismi di overriding ed overload hanno un ruolo fontamentale nell'implementazione del polimorfismo come si vede nel seguente esempio:
class polimorfismo{
public static void main (String args []){
System.out.println(somma("due","tre"));
System.out.println(somma(2,3));
}//fine main
static int somma(int a,int b){ return a+b;}
static String somma(String a,String b){ return a+b;}
}// fine classe
l'output sarebbe:
duetre
5
Esistono due metodi che hanno lo stesso identificatore ( nome ) ma differiscono
per il tipo dei parametri passati.
Il corpo del metodo è lo stesso ma uno esegue una somma fra numeri mentre
l'altro esegue una concatenazione fra stringhe.
Si parla di somma() come di un metodo
sovraccarico ( overload ) e si tratta certamente di un caso di polimorfismo
dato che la funzione somma() può assumere un comportamento
o un altro a secondo dei parametri passati. Un altro esempio è il seguente;
class overload {
public static void main(String[] args) {
D dx=new D();
D dy=new D(5);
System.out.println(dx.get());
System.out.println(dy.get());
} //fine main
}//fine classe
class D {
private int p;
D(){p=lancia();} //I° costruttore
D(int j){p=j*lancia();} //II° costruttore
private int lancia(){ return (1+(int)(Math.random()* 6));}
int get(){return p;}
}//fine classe D
l'output potrebbe essere:
4
30
Si tratta di un programma che simula il risultato del lancio di un
dado nel caso venga invocato il costruttore D().
Ma quando viene chiamato D(5)viene ottenuto il risultato
del lancio di un dado moltiplicato per 5.
L'overload dei metodi consiste nel creare più
metodi con lo stesso nome ma con segnatura diversa
( numero e ordine dei parametri passati ).
Sarà eventualmente il compilatore a decidere in fase di run-time (late-binding)
quale metodo applicare a secondo del tipo di parametri ricevuti in input.
Il seguente programma simula invece il lancio di un dado e di due dadi in successione.
class override {
public static void main(String[] args) {
D d=new D();
DD dd=new DD();
System.out.println(d.get());
System.out.println(dd.get());
} //fine main
}//fine classe
class D {
protected int p;
D(){p=lancia();}
int lancia(){ return (1+(int)(Math.random()* 6));}
int get(){return p;}
}//fine classe D
class DD extends D{
DD(){p=lancia();}
int lancia(){ //usa il metodo della classe D e lo
riscrivo
int j;
j=super.lancia();
j+=super.lancia();
return j;
}//fine lancia
}//fine classe DD
un risultato possibile sarebbe:
2
9
La classe D simula il lancio di un dado attraverso il metodo lancio().
La classe DD eredita dalla classe D tutto: l'attributo p,
il metodo get() e anche il metodo lancia()
e lo sovrascrive , cioè lo riutilizza e simultaneamente lo ridefinisce:
questo è un esempio di override di un
metodo.
Modificatori di accesso
Qui di seguito sono riportati i modificatori di visibilità con
la seguente convenzione di applicabilità:
C=classe
I=interfaccia
M=metodo o costruttore
A=attributo
public (C-I-M-A) entità pubblica visibile da tutti gli altri package.
private (C-I-M-A) entità privata visibile solo nella classe che contiene la definizione.
protected (C-I-M-A) entità protetta visibile nel package corrente e da tutte le sottoclassi della classe che contiene la definizione.
static (C-M-A) di metodi e attributi statici avevamo già accennato,le classi statiche possono essere definite solo all'interno di altri classi e consentono una soluzione alternativa al package.
final (C-M-A) Per i metodi: non ridefinibile nelle sottoclassi. Per gli attributi: assegnabile una sola volta. Raro per le classi.
abstract (C-I-M) definisce un metodo o una classe astratta.
sychronized (M) Un metodo sincronizzato non può esssere eseguito da più thread contemporaneamente.
native (M) Il metodo non è implementato in Java ma nel linguaggio nativo della piattaforma sottostante.
volatile (A) comunica al compilatore che questo attributo può essere usato da più threads, non effettuare ottimizzazioni su esso perchè ciò potrebbe alterare la semantica desiderata.
transient (A) L'attributo non deve essere tenuto in considerazione nei processi di memorizzazione persistente dell'oggetto.
strictfp (C-I-M) Richiede che i calcoli aritmetici in virgola mobile vengano eseguiti rigorosamente secondo lo standard IEEE 754.