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
2 - Unità didattica 6.1.2)
- Casting di oggetti:
Nell’esempio precedente, abbiamo osservato che
l’operatore instanceof ci permette di testare, a quale tipo di
istanza punta un reference. Precedentemente però, abbiamo anche
notato che il polimorfismo per dati, quando implementato, fa sì che
il reference che punta ad un oggetto istanziato da una sottoclasse,
non possa accedere ai membri dichiarati nelle sottoclassi stesse.
Esiste però la possibilità di ristabilire la piena funzionalità
dell’oggetto tramite il meccanismo del casting di oggetti.
Rifacciamoci all’esempio appena presentato. Supponiamo che lo
stipendio di un programmatore dipenda dal numero di anni di
esperienza. In questa situazione, dopo aver testato che il reference
dip punta ad un’istanza di Programmatore, avremo bisogno di accedere
alla variabile anniDiEsperienza. Se tentassimo di accedervi mediante
la sintassi dip.anniDiEsperienza(), otterremo sicuramente un errore
in compilazione. Ma se utilizziamo il meccanismo del casting di
oggetti, supereremo anche quest’ultimo ostacolo. In pratica
dichiareremo un reference a Programmatore, e lo faremo puntare
all’indirizzo di memoria dove punta il reference dip, utilizzando il
casting per confermare l’intervallo di puntamento. A questo punto,
il nuovo reference, essendo un reference "giusto", ci permetterà di
accedere a qualsiasi membro dell’istanza di Programmatore. Il
casting di oggetti sfrutta una sintassi del tutto simile al casting
tra dati primitivi:
if (dip instanceof programmatore)
{
Programmatore pro = (Dipendente) dip;
. . .
Siamo ora in grado di accedere alla variabile anniDiEsperienza,
tramite la sintassi:
. . .
if (pro.anniDiEsperienza>2)
. . .
Schematizziamo la situazione con il seguente grafico:

N.B.: notare come i due reference abbiano lo
stesso valore numerico (indirizzo), ma differente intervallo di
puntamento, ed è questo che ne stabilisce la funzionalità. Nel caso
tentassimo di assegnare al reference pro l’indirizzo di dip senza
l’utilizzo del casting, otterremmo un errore in compilazione ed un
relativo messaggio che ci richiede un casting esplicito. Ancora una
volta, il comportamento del compilatore è in linea con la robustezza
del linguaggio. Nell’ambito compilativo, il compilatore non può
stabilire se ad un certo indirizzo risiede un determinato oggetto
piuttosto che un altro. È solo nell’ambito esecutivo che Java
Virtual Machine può sfruttare l’operatore instanceof per risolvere
il dubbio.
N.B.: la nostra personale esperienza, ci porta a
considerare il casting di oggetti, non come strumento standard di
programmazione, ma piuttosto come un’utile strumento per risolvere
problemi progettuali. Una progettazione ideale, farebbe a meno del
casting di oggetti. Per quanto ci riguarda, all’interno di un
progetto, la necessità del casting ci porta a pensare ad una
"forzatura" e quindi ad un’eventuale aggiornamento della
progettazione.
N.B.: ancora una volta osserviamo un altro
aspetto che fa definire Java come linguaggio semplice. Il casting è
un argomento che esiste anche in altri linguaggi, e riguarda i tipi
di dati primitivi numerici. Esso viene realizzato troncando i bit in
eccedenza di un tipo di dato il cui valore vuole essere forzato ad
entrare in un altro tipo di dato "più piccolo". Notiamo che nel caso
di casting di oggetti, non viene assolutamente troncato nessun bit,
e quindi si tratta di un processo completamente diverso! Se però ci
astraiamo dai tipi di dati in questione, la differenza non sembra
sussistere, e Java permette di utilizzare la stessa sintassi,
facilitando l’apprendimento e l’utilizzo al programmatore.
- Metodi virtuali:
Un metodo m, è può definirsi virtuale, quando è
definito in una classe A, ridefinito in una sottoclasse B (override),
ed è invocato su un’istanza di B, tramite un reference di A
(polimorfismo per dati). Quando s’invoca un metodo virtuale, il
compilatore "pensa" di invocare il metodo m della classe A
(virtualmente), ma, in realtà, viene invocato il metodo ridefinito
nella classe B. Un esempio classico è quello del metodo toString()
della classe Object. Abbiamo già accennato al fatto che esso è in
molte classi della libreria standard "overridato". Consideriamo la
classe Date del package java.util. In essa, il metodo toString() è
riscritto in modo tale da restituire informazioni sull’oggetto Date
(giorno, mese, anno, ora, minuti, secondi, giorno della settimana,
ora legale…). Consideriamo il seguente frammento di codice:
. . .
Object obj = new Date();
String s1 = obj.toString();
. . .
Il reference s1, conterrà la stringa che contiene
informazioni riguardo l’oggetto Date, che è l’unico oggetto
istanziato. Schematizziamo:

Il reference obj, può accedere solamente
all’interfaccia pubblica della classe Object, e quindi anche al
metodo toString(). Il reference punta però, ad un’area di memoria,
dove risiede un oggetto della classe Date, dove il metodo toString()
ha una diversa implementazione.
- Esempio d’utilizzo del polimorfismo:
Supponiamo di avere a disposizioni le seguenti classi:
class Veicolo
{
public void accelera()
{
. . .
}
public void decelera()
{
. . .
}
}
class Aereo extends Veicolo
{
public void decolla()
{
. . .
}
public void atterra()
{
. . .
}
public void accelera()
{
// ridefinizione del metodo ereditato
. . .
}
public void decelera()
{
// ridefinizione del metodo ereditato
. . .
}
. . .
}
class Automobile extends Veicolo
{
public void accelera()
{
// ridefinizione del metodo ereditato
. . .
}
public void decelera()
{
// ridefinizione del metodo ereditato
. . .
}
public void innestaRetromarcia()
{
. . .
}
. . .
}
class Nave extends Veicolo
{
public void accelera()
{
// ridefinizione del metodo ereditato
. . .
}
public void decelera()
{
// ridefinizione del metodo ereditato
. . .
}
public void gettaAncora()
{
. . .
}
. . .
}
La superclasse Veicolo definisce i metodi
accellera e decelera, che vengono poi ridefinite in sottoclassi più
specifiche quali Aereo, Automobile e Nave. Consideriamo la seguente
classe che fa uso dell’overload:
class Viaggiatore
{
public void viaggia(Automobile a)
{
a.accelera();
. . .
}
public void viaggia(Aereo a)
{
a.accelera();
. . .
}
public void viaggia(Nave n)
{
n.accelera();
. . .
}
. . .
}
Nonostante l’overload, rappresenti una soluzione
notevole per una potente codifica della classe Viaggiatore, notiamo
una certa ripetizione nei blocchi di codice dei tre metodi.
Sfruttando infatti un parametro polimorfo ed un metodo virtuale, la
classe Viaggiatore, potrebbe essere codificata in modo più compatto
e funzionale:
class Viaggiatore
{
public void viaggia(Veicolo v)//param. polimorfo
{
v.accelera(); //metodo virtuale
. . .
}
. . .
}
Il seguente frammento di codice infatti, utilizza le precedenti
classi:
Viaggiatore claudio = new Viaggiatore();
Automobile fiat500 = new Automobile();
// avremmo potuto istanziare anche una Nave o un Aereo
claudio.viaggia(fiat500);
Notiamo la chiarezza e la versatilità del codice.
Conclusioni:
In questo modulo abbiamo potuto apprezzare il polimorfismo ed i suoi
molteplici aspetti. Speriamo che il lettore abbia appreso almeno le
definizioni presentate in questo modulo ed intuitone l’utilità. Non
sarà sicuramente immediato, imparare ad utilizzare correttamente i
potenti strumenti della programmazione ad oggetti. Certamente può
aiutare molto conoscere una metodologia Object Oriented, o, almeno,
U.M.L. . L’esperienza rimane come sempre, la migliore "palestra".
Nel prossimo modulo sarà presentato un esercizio guidato, che vuole
essere un primo passo verso una corretta utilizzazione della
programmazione ad oggetti.
Introduzione e Unità didattica 6.1.1) -
Pagina 1
Unità didattica 6.1.2) - Pagina 2