Buono sconto 4% su Toner e Cartucce agli utenti AZpoint. SU Iomiricarico.it!!
- 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.
|