Il linguaggio Java: lez.7
1 - Introduzione e Unità
didattica 6.1.1)
Programmazione ad oggetti
utilizzando Java:
Polimorfismo
1
|
2 |
3 |
4
Introduzione e Unità didattica 6.1.1) -
Pagina 1
Unità didattica 6.1.2) - Pagina 2
Obiettivi:
Il Lettore al termine di questo capitolo dovrà essere in grado di
- Comprendere il significato del polimorfismo (unità 6.1).
- Saper utilizzare l’overload, l’override ed il polimorfismo per
dati (unità 6.1).
- Comprendere e saper utilizzare le collezioni eterogenee,i
parametri polimorfi, ed i metodi virtuali (unità 6.1).
- Sapere utilizzare l’operatore instanceof, ed il casting di
oggetti (unità 6.1).
Unità didattica 6.1)
- Polimorfismo
Il polimorfismo (dal greco "molte forme") è un altro concetto che
dalla realtà, è stato importato nella programmazione ad oggetti.
Esso ci permette di riferirci con un unico termine a "entità"
diverse. Ad esempio, sia un telefono fisso, sia un portatile ci
permettono di telefonare, dato che entrambi i mezzi sono definibili
come telefoni. Telefonare quindi, può essere considerata un’azione
polimorfica (ha diverse implementazioni). Ma il polimorfismo in
Java, è argomento complesso che si dirama in vari sotto-argomenti.
Utilizzando una convenzione con la quale rappresenteremo con
rettangoli i concetti, e con ovali concetti che hanno una reale
implementazione in Java, cercheremo di schematizzare il polimorfismo
e le sue espressioni.

- Convenzione per i reference:
Prima di iniziare a definire i vari aspetti del
polimorfismo, presentiamo una convenzione per la definizione di una
variabile di tipo reference. Definire precisamente cosa sia un
reference, e come sia rappresentato in memoria, non è cosa semplice.
Di solito s’identifica un reference con un puntatore. In molti testi
relativi ad altri linguaggi di programmazione, un puntatore viene
definito come "una variabile che contiene un indirizzo". In realtà
la definizione di puntatore cambia da piattaforma a piattaforma. Per
convenzione possiamo definire un reference come una variabile che
contiene due informazioni rilevanti: l’indirizzo in memoria, e
l’intervallo di puntamento definito dalla relativa classe. Per
esempio se scriviamo
Punto ogg = new Punto();
possiamo supporre che il reference ogg abbia come indirizzo un
valore numerico, ad esempio 10023823, e come intervallo di
puntamento Punto. In particolare, ciò che abbiamo definito come
intervallo di puntamento farà sì che il reference ogg possa accedere
all’interfaccia pubblica della classe Punto, ovvero, a tutti i
membri pubblici (x, y, distanzaDallOrigine) dichiarati nella classe
Punto, tramite il reference ogg. L’indirizzo invece farà puntare il
reference ad una particolare area di memoria dove risiederà il
particolare oggetto istanziato. Di seguito viene riportato uno
schema rappresentativo:

- Polimorfismo per metodi:
Il polimorfismo per metodi, per quanto detto sino
ad ora, ci permetterà di utilizzare lo stesso nome per metodi
differenti. In Java, esso trova una sua realizzazione pratica sotto
due forme: l’overload (che potremmo tradurre con "sovraccarico") e
l’override (che potremmo tradurre con "riscrittura"). - Overload:
Definizione 1:
In un metodo, la coppia costituita dall’identificatore e dalla lista
dei parametri è detta "segnatura" o "firma" del metodo.
In Java, un metodo è univocamente determinato non solo dal suo
identificatore, ma anche dalla sua lista di parametri, cioè dalla
sua segnatura. Quindi, in una classe, possono convivere metodi con
lo stesso nome, ma con differente firma. Su questo semplice concetto
si fonda una delle caratteristiche più utili di Java: l’overload.
Tramite esso il programmatore potrà utilizzare lo stesso nome per
metodi diversi. Ovviamente tutto ciò deve avere un senso logico, un
significato. Per esempio potremmo assegnare lo stesso nome a due
metodi che concettualmente hanno la stessa funzionalità, ma che
soddisfano tale funzionalità in maniera differente. Presentiamo di
seguito, un banale esempio di overload:
class Aritmetica
{
public int somma(int a, int b)
{
return a+b;
}
public float somma(int a, float b)
{
return a+b;
}
public float somma(float a, int b)
{
return a+b;
}
public int somma(int a, int b, int c)
{
return a+b+c;
}
public double somma(int a, double b, int c)
{
a+b+c;
}
}
In questa classe ci sono ben cinque metodi che
hanno lo stesso nome, svolgono delle somme, ma in modo differente.
Se invece volessimo implementare questi metodi in un altro
linguaggio che non supporta l’overload, dovremmo inventare un nome
nuovo per ogni metodo. Presumibilmente il primo di essi si potrebbe
chiamare "sommaDueInt", il secondo "sommaUnIntEUnFloat", il terzo "sommaUnFloatEUnInt",
il quarto "sommaTreInt", il quinto addirittura "sommaUnIntUnDoubleEUnFloat"!
A questo punto pensiamo sia evidente al lettore l’utilità dell’overload.
Notiamo che la lista dei parametri ha tre criteri di distinzione:
tipale (Es.: somma(int a, int b) è diverso da somma(int a, float b))
numerico (Es.: somma(int a, int b) è diverso da somma(int a, int b,
int c))
posizionale (Es.: somma(int a, int b) è diverso da somma(float a,
int b))
Gli identificatori che utilizziamo per i parametri, non sono quindi
criteri di distinzione per i metodi (Es.: somma(int a, int b) non è
diverso da somma(int c, int d)).
N.B.: il tipo di ritorno non fa parte della firma di un metodo,
quindi non ha importanza per l'argomento overload.
N.B.: In alcuni testi, l’overload non è considerato come aspetto
polimorfico di un linguaggio. In questi testi il polimorfismo stesso
è definito in maniera diversa da com’è stato definito in questo
contesto. Come sempre, è tutto relativo all’ambito in cui ci si
trova. Se ci fossimo trovati a discutere anziché di programmazione,
di analisi e progettazione object oriented, probabilmente neanche
noi avremmo inserito l’overload come argomento del polimorfismo.
- Override:
L’override, e questa volta non ci sono dubbi, è invece considerata
una potentissima caratteristica della programmazione ad oggetti, ed
è da qualcuno superficialmente identificato con il polimorfismo
stesso. L’override (sovra-scrittura) è il termine object oriented
che viene utilizzato per descrivere la caratteristica che hanno le
sottoclassi, di ridefinire un metodo ereditato da una superclasse.
Ovviamente, non esisterà override senza ereditarietà. Una
sottoclasse non è mai meno specifica di una classe che estende, e
quindi, potrebbe ereditare metodi che hanno bisogno di essere
ridefiniti per funzionare correttamente nel nuovo contesto.
Ad esempio supponiamo che una classe Punto (che per convenzione
assumiamo bidimensionale), dichiari un metodo DistanzaDallOrigine
che calcola, con la nota espressione geometrica, la distanza tra un
punto di determinate coordinate dall'origine degli assi cartesiani.
Ovviamente questo metodo ereditato all’interno di un’eventuale
classe PuntoTridimensionale ha bisogno di essere ridefinito per
calcolare la distanza voluta, tenendo conto anche della terza
coordinata. Vediamo quanto appena detto sotto forma di codice. Per
semplicità il nostro codice non farà uso "dell’obbligatorio
incapsulamento".
N.B.: il lettore è invitato a riflettere sulle discutibili scelte di
chiamare una classe che astrae un punto bidimensionale "Punto", e di
inserire il metodo DistanzaDallOrigine nella stessa classe. Non
stiamo così violando il paradigma dell’astrazione?
class Punto
{
public int x, y;
public double distanzaDallOrigine()
{
int tmp=(x*x) + (y*y);
return Math.sqrt(tmp);
}
}
class PuntoTridimensionale extends Punto
{
int z;
public double distanzaDallOrigine()
{
int tmp=(x*x) + (y*y) + (z*z);
return Math.sqrt(tmp);
}
}
N.B.: il metodo sqrt() della classe Math (package
java.lang), restituisce un valore double risultato della radice
quadrata del parametro passato (il lettore è invitato sempre e
comunque a consultare la documentazione). E’ stato possibile
invocarlo con la sintassi nomeClasse.nomeMetodo anziché
nomeOggetto.nomeMetodo perché trattasi di un metodo statico.
Discuteremo nel modulo 9 in dettaglio i metodi statici. Il lettore
si accontenti per il momento di sapere che un metodo statico
"appartiene alla classe".
Si può osservare come è stato possibile ridefinire il blocco di
codice del metodo DistanzaDallOrigine, per introdurre le terza
coordinata di cui tenere conto, affinché il calcolo della distanza
sia eseguito correttamente nella classe PuntoTridimensionale.
È bene notare che ci sono delle regole da rispettare per l’override.
1) se decidiamo di riscrivere un metodo in una sottoclasse, dobbiamo
utilizzare la stessa identica segnatura, altrimenti utilizzeremo un
overload in luogo di un override.
2) il tipo di ritorno del metodo deve coincidere con quello del
metodo che si sta riscrivendo.
3) il metodo ridefinito, non deve essere meno accessibile del metodo
che ridefinisce. Per esempio se un metodo ereditato è dichiarato
protetto, non si può ridefinire privato, ma semmai, pubblico.
- Override & classe Object: metodo toString()
Abbiamo detto che la classe Object è la superclasse di tutte le
classi. Ciò significa che quando codificheremo una classe qualsiasi,
erediteremo tutti gli 11 metodi di Object. Tra questi c’è il metodo
toString() che avrebbe il compito di restituire una stringa
descrittrice dell’oggetto. Nella classe Object, che astrae il
concetto di oggetto generico, tale metodo non poteva adempiere
questo scopo, giacché un’istanza della classe Object non ha
variabili d’istanza che lo caratterizzano. E’ quindi stato deciso di
implementare il metodo toString() in modo tale che restituisca una
stringa contenente informazioni sul reference del tipo:
nomeClasse@indirizzoInEsadecimale
che per la convenzione definita in precedenza potremmo interpretare
come:
intervalloDiPuntamento@indirizzoInEsadecimale
Per esercizio, il lettore può provare a stampare un reference
qualsiasi.
Un altro metodo che degno di nota, è il metodo
equals().Esso è destinato a confrontare tra due reference (sul primo
è chiamato il metodo e il secondo viene passato come parametro), e
restituisce un valore booleano true se e solo se i due reference
puntano ad uno stesso oggetto. Ma questo tipo di confronto, come
abbiamo avuto modo di costatare nel modulo 3, è fattibile anche
mediante l’operatore "==". Allora, in molte sottoclassi di Object,
come ad esempio String, il metodo equals() è stato riscritto in modo
tale da restituire true anche nel caso di confronto tra due
reference che puntano ad oggetti diversi, ma con gli stessi
contenuti. È molto importante tenere conto di quanto appena detto,
per non perdere ore preziose in debug evitabili.
- Polimorfismo per dati (per classi):
Il polimorfismo per dati, permette essenzialmente
di poter assegnare un reference di una superclasse ad un’istanza di
una sottoclasse. Per esempio, tenendo conto che PuntoTridimensionale
è sottoclasse di Punto, sarà assolutamente legale scrivere:
Punto ogg = new PuntoTridimensionale();
Il reference ogg, infatti, punterà ad un
indirizzo che valida il suo intervallo di puntamento. Praticamente
l’interfaccia pubblica dell’oggetto creato (costituita dalle
variabili x, y e z) contiene l’interfaccia pubblica della classe
Punto (costituita dalle variabili x e y), e così il reference ogg
"penserà" di puntare ad un oggetto Punto. Se volessimo rappresentare
graficamente questa situazione, potremmo basarci sulla seguente
figura:

Questo tipo d’approccio ai dati ha però un
limite. Un reference di una superclasse, non potrà accedere ai campi
dichiarati per la prima volta nella sottoclasse. Nell’esempio
otterremmo un errore in compilazione se tentassimo quindi di
accedere alla terza coordinata Z del PuntoTridimensionale, tramite
il reference ogg. In pratica, la codifica della seguente riga:
ogg.z=5;
produrrebbe un errore in fase di compilazione, dal momento che ogg è
un reference che ha un intervallo di puntamento di tipo Punto.
Il lettore è rimandato al termine di questo modulo per la conoscenza
delle fondamentali conseguenze che tale approccio ai dati
comporterà.
- Parametri polimorfi:
Sappiamo che i parametri in Java sono sempre passati per valore. Ciò
implica che passare un parametro di tipo reference ad un metodo,
significa passare il valore numerico del reference, in altre parole,
il suo indirizzo. A quest’indirizzo potrebbe risiedere un oggetto
istanziato da una sottoclasse, grazie al polimorfismo per dati.
In un metodo, un parametro di tipo reference, si dice polimorfo,
quando, anche essendo di fatto un reference relativo ad una
determinata classe, può puntare ad un oggetto istanziato da una
sottoclasse. In pratica sfruttando il polimorfismo per dati, un
parametro di un metodo potrebbe in realtà puntare ad oggetti
diversi.
1) public void stampaOggetto(Object ogg)
{
System.out.println(ogg.toString());
}
Ora il lettore sa bene che tutte le classi sono sottoclassi di
Object. Quindi potremmo chiamare il metodo stampaOggetto,
passandogli come parametro anziché un’istanza di Object, un’istanza
di String come "Ciao". Ma a questo tipo di metodo possiamo passare
un’istanza di qualsiasi classe, dal momento che vale il polimorfismo
per dati, ed ogni classe è sottoclasse di Object.
N.B.: ogni classe eredita dalla classe Object il metodo toString.
Molte classi della libreria standard di Java fanno un override di
questo metodo, restituendo stringhe descrittive dell’oggetto. Se al
metodo stampaOggetto passassimo come parametro un oggetto di una
classe che non ridefinisce il metodo toString, sarebbe chiamato il
metodo toString ereditato dalla classe Object.
N.B.: il frammento di codice 1) è assolutamente equivalente al
seguente:
2) public void stampaOggetto(Object ogg)
{
System.out.println(ogg);
}
infatti il metodo println utilizzato nel codice 2) è diverso da
quello utilizzato nel codice 1). Trattasi infatti, di un classico
esempio di overload: nel codice 1) è utilizzato il metodo println
che prende come parametro una stringa:
println(String)
mentre nel codice 2) è utilizzato il metodo println che prende come
parametro un Object (e quindi un qualsiasi oggetto):
println(Object)
e che stamperà una descrizione dell’oggetto passato utilizzando
proprio il metodo toString().
- Collezioni eterogenee:
Una collezione eterogenea, è una collezione composta da oggetti
diversi (ad esempio un array di Object che in realtà immagazzina
oggetti diversi). Anche la possibilità di sfruttare collezioni
eterogenee, è garantita dal polimorfismo per dati. Infatti, un array
dichiarato di Object potrebbe contenere ogni tipo di oggetto, per
esempio:
Object arr[] = new Object[3];
arr[0] = new Punto(); //arr[0], arr[1], arr[2]
arr[1] = "Hello World!"; //sono reference ad Object
arr[2] = new Date(); //che puntano ad oggetti
//istanziati da sottoclassi
il che equivalente a:
Object arr[]={ new Punto(),"Hello World!",new Date()} ;
Presentiamo di seguito un esempio allo scopo di
intuire la potenza e l’utilità di questi concetti. Immaginiamo di
voler realizzare un sistema che stabilisca le paghe dei dipendenti
di un’azienda, considerando le seguenti classi:
class Dipendente
{
String nome;
int stipendio;
int matricola;
String dataDiNascita;
String dataDiAssunzione;
}
class Programmatore extends Dipendente
{
String linguaggiConosciuti;
int anniDiEsperienza;
}
class Dirigente extends Dipendente
{
String orarioDiLavoro;
}
class AgenteDiVendita extends Dipendente
{
String [] portafoglioClienti;
int provvigioni;
}
. . .
Il nostro scopo è di realizzare una classe che
stabilisca le paghe dei dipendenti. Potremmo ora utilizzare una
collezione eterogenea di dipendenti, ed un parametro polimorfo per
risolvere il problema in un modo molto semplice, veloce ed elegante.
Infatti, potremmo dichiarare una collezione eterogenea di
dipendenti:
Dipendente [] arr = new Dipendente [180];
arr[0]=new Dirigente();
arr[1]=new Programmatore();
arr[2]=new AgenteDiVendita();
. . .
Esiste tra gli operatori di Java, un operatore
binario costituito da lettere: instanceof. Tramite esso si può
testare a che tipo di oggetto in realtà un reference punta:
public void pagaDipendente(Dipendente dip)
{
if (dip instanceof programmatore)
{
dip.stipendio=2000000;
}
else if (dip instanceof Dirigente)
{
dip.stipendio=5000000;
}
else if (dip instanceof AgenteDiVendita)
{
dip.stipendio=1000000;
. . .
}
}
Ora, possiamo chiamare questo metodo all’interno
di un ciclo di 180 iterazioni, passandogli tutti gli elementi della
collezione eterogenea, e raggiungere così il nostro scopo:
. . .
for (int i=0; i<180; i++)
{
pagaDipendente(arr[i]);
. . .
}
Introduzione e Unità didattica 6.1.1) -
Pagina 1
Unità didattica 6.1.2) - Pagina 2