MokaByte 68 - 9mbre 2002 
Un'introduzione al paradigma ad oggetti attraverso lo schema "Kernel-Modulo"
III parte: overriding e overloading
di
Matteo Baldoni
Nel precedenti due articoli abbiamo introdotto le caratteristiche principali dei linguaggi orientati agli oggetti, ed in particolare di Java, attraverso lo schema "Kernel-Modulo" ed abbiamo discusso i vantaggi e svantaggi dell'uso del typechecking statico di Java come "documentazione interna al programma". In questo articolo completiamo la nostra introduzione al paradigma ad oggetti e al linguaggio Java attraverso lo schema kernel-Modulo soffermandoci sui concetti di overloading e overriding.

Lo schema Kernel-Modulo Dinamico
Lo schema "Kernel-Modulo Dinamico" (KMD in breve), rappresentato in Figura 1, è caratterizzato da una classe, che chiameremo ClasseKernel che definisce un metodo, metodoKernel, nel cui corpo è utilizzato un oggetto (obj) di un'altra classe, InterfacciaModulo, invocando un suo metodo, metodoModulo. Normalmente la classe InterfacciaModulo è un'interfaccia implementata da una o più classi, nella figura ImplUnoModulo e ImplDueModulo. La combinazione delle classi ClasseKernel e InterfacciaModulo rappresentano il "Kernel" mentre le classi che implementano InterfacciaModulo,nell'esempio ImplUnoModulo e ImplDueModulo, rappresentano i "Moduli".


Figura 1 - Lo schema "Kernel-Modulo Dinamico".
(clicca sull'immagne per ingrandirla)


Typeckecking statico: sovraccarico (overloading) e ridefinizione (overriding)
In Java il controllo dei tipi è effettuato a tempo di compilazione (tychecking statico), questo significa che il compilatore si preoccupa di verificare che un certo oggetto disponga effettivamente del metodo invocato su di esso. Il controllo avviene sulla base del tipo della variabile che conterrà il riferimento all'oggetto e non sul tipo effettivo dell'oggetto stesso (eventualmente più specifico, cioè un sottotipo) che sarà noto solo a tempo di esecuzione. Se l'oggetto puntato dalla variabile durante l'esecuzione ha tipo più specifico rispetto al tipo della variaile stessa e per esso è disponibile una ridefinizione del metodo associato durante la compilazione, il meccanismo del binding dinamico assicura che quest'ultimo sia quello utilizzato. In altre parole il binding dinamico permette di scegliere tra le possibili ridefinizioni di un metodo quella da applicare. Il punto chiave per comprendere e riuscire a sfruttare bene dal punto di vista pratico questo meccanismo è capire cosa si intende esattamente per ridefinizione. In Java, a differenza di altri linguaggi orientati agli oggetti, un metodo di una sottoclasse ridefinisce un metodo di una sua sopraclasse se ha il suo stesso nome e identica lista di tipi di parametri.
Per approfondire questo tema, consideriamo una prima semplice variante nello schema KMD precedentemente illustrato. Supponiamo che il metodo esterno metodoModulo utilizzato in ClasseKernel abbia un parametro formale c di tipo ClasseUno e che la classe Modulo ImplUnoModulo disponga, oltre che dell'implementazione del metodo esterno metodoModulo con parametro di tipo ClasseUno, anche di un altro metodo avente sempre nome metodoModulo ma con un parametro di tipo ClasseDue e il cui prototipo non è presente in InterfacciaModulo. Tecnicamente si dice che il metodo metodoModulo in ImplUnoModulo è sovraccarico (overloaded), si veda la Figura 3.


Figura 2 - Lo schema "Kernel-Modulo Dinamico", una semplice variante
(clicca sull'immagne per ingrandirla)

Supponiamo, inoltre, che ClasseDue sia un'estensione di ClasseUno (ClasseDue è sottotipo di ClasseUno) e consideriamo il seguente frammento di codice di un metodo della ClasseKernel.


/* File: ClasseKernel.java */
public class ClasseKernel {
public InterfacciaModulo obj;
...
public metodoKernel() {
ClasseUno c;
...
obj.metodoModulo(c);
...
}
...
}

Consideriamo l'istruzione obj.metodoModulo(c) dell'esempio. In fase di compilazione il typechecker verificherà che essendo obj di tipo InterfacciaModulo e c di tipo ClasseUno, esista una definizione del metodo metodoModulo che si applica al caso, cosa che per l'esempio è vera in quanto esiste proprio un metodo con quel nome e con parametro formale di tipo ClasseUno nella definizione di InterfacciaModulo. Osserviamo che nel nostro caso esistono ben due implementazione dell'interfaccia in questione, quindi vi sono due differenti implementazione di metodoModulo applicato a ClasseUno. A tempo di esecuzione il meccanismo di binding dinamico farà si che il codice associato alla chiamata sia quello del metodo ridefinito nella calsse del tipo dell'oggetto puntato da obj, ad esempio in ImplUnoModulo.
Ma cosa succede se il parametro attuale della chiamata avesse tipo ClasseDue?

/* File: ClasseKernel.java */
public class ClasseKernel {
public InterfacciaModulo obj;
...
public metodoKernel() {
ClasseDue c;
...
obj.metodoModulo(c);
...
}
...
}

Sarebbe ragionevole aspettarsi che durante l'esecuzione, venga associato alla chiamata il codice del metodo metodoModulo avente parametro formale di tipo ClasseDue. Questo però non accade e il metodo selezionato sarà quello dell'esempio precedente. Il motivo è semplice, questo secondo metodo nella classe ImplUnoModulo non è una ridefinizione del prototipo specificato in InterfacciaModulo e quindi il binding dinamico non la considera. L'unica maniera per effettuare l'associazione desiderata sarebbe effettuare un esplicito downcast durante la chiamata ((ImplUnoModulo)obj).metodoModulo(c). In questo modo fin dalla fase di compilazione il metodo con parametro formale di tipo ClasseDue risulterebbe associato alla nostra chiamata. Un'alternatica a questa soluzione "sporca" (che tra l'altro non è consigliabile come soluzione visto che costringe alla modifica diretta del codice del Kernel!) consiste nell'introduzione della segnatura di questo nuovo metodo nell'interfaccia InterfacciaModulo; questo risolverebbe il problema senza la necessità di un downcast specifico al tipo della classe Modulo utilizzata.
In questa nuova variante i metodi esterni utilizzabili nella ClasseKernel e definiti in InterfacciaModulo sono due, entrambi hanno lo stesso nome ma tipi di parametri diversi (overloading). I due metodi definiti in InterfacciaModulo differiscono tra di loro dalla presenza di un parametro di tipo ClasseUno per il primo e un parametro di tipo ClasseDue per il sencondo, dove la classe ClasseDue estende la classe ClasseUno. Ovviamente, in questo caso, ogni classe Modulo, come nell'esempio ImplUnoModulo e ImplDueModulo, dovranno necessariamente implementare entrambi i metodi che ridefiniranno i prototipi in InterfacciaModulo (overriding). Si noti che, seguendo quanto detto prima sul concetto di ridefinizione in Java, ognuno dei metodo nel Modulo ridefinisce il corrispondente prototipo in InterfacciaModulo, come mostrato in Figura 3.


Figura 3 - Lo schema "Kernel-Modulo Dinamico", un'altra variante.
(clicca sull'immagne per ingrandirla)

Consideriamo la seguente ulteriore variante del frammento del codice di un metodo della ClasseKernel. In questo caso la variabile che conterrà il parametro attuale è di tipo ClasseUno ma a tempo di esecuzione sicuramente farà riferimento ad un oggetto di tipo ClasseDue.

/* File: ClasseKernel.java */
public class ClasseKernel {
public InterfacciaModulo obj;
...
public metodoKernel() {
ClasseUno c;
...
c = new ClasseDue();
...
obj.metodoModulo(c);
...
}
...
}

Quale codice viene associato a tempo di esecuzione alla chiamata dell'istruzione obj.metodoModulo(c)? Il fatto che abbiamo indicato che l'oggetto effettivamente puntato dal parametro attuale c a tempo di esecuzione sia un oggetto di tipo ClasseDue non cambia il ragionamento presentato precedentemente. Durante la compilazione il compilatore determina il tipo del parametro attuale c della chiamata (ClasseUno) e il tipo della variabile obj (InterfacciaModulo). Quindi verifica che in InterfacciaModulo esista un metodo di nome metodoModulo con un parametro di tipo ClasseUno e questo viene trovato. A tempo di esecuzione, per il meccanismo del binding dinamico, si associa il codice contenuto in una possibile ridefinizione del metodo determinato a tempo di compilazione presente nella classe che definisce tipo reale dell'oggetto puntato da obj. Poichè per ridefinizione si intende un metodo con stesso nome e stessa lista di tipi di parametri il metodo associato a tempo di esecuzione sarà il metodo metodoModulo della classe ImplUnoModulo con parametro formale un oggetto di tipo ClasseUno anche se ituitivamente avremmo pensato a quello con parametro formale un oggetto di tipo ClasseDue. In pratica per capire quale metodo verrà usato bisogna considerare il tipo, a tempo di esecuzione, dell'oggetto su cui il metodo è invocato; quindi occorre cercare nella classe così determinata un metodo avente nome identico a quello invocato e lista dei tipi degli argomenti corrispondente alla lista dei tipi determinati a tempo di compilazione per quella chiamata.
Riassumendo, il binding dinamico determina il codice da associare ad una chiamata di un metodo tenendo conto del tipo dell'oggetto su cui è invocato il metodo stesso durante l'esecuzione e del tipo statico associato ai suoi parametri attuali durante la compilazione. Vi è quindi un differente trattamento per quello che è considerato il parametro implicito (l'oggetto a cui si invia il messaggio) e i parametri espliciti di un metodo. Tale differenza di trattamento può spesso causare problemi, infatti è come se avessimo una chiamata di una funzione metodoModulo(obj, c), con riferimento all'esempio, in cui il tipo di obj è determinato dinamicamente e quello di c staticamente! In Python, un linguaggio di programmazione di scripting orientato agli oggetti, ad esempio, la scelta di non introdurre tipi espliciti e non permettere l'overloading porta a non avere tali problemi; tuttavia al contempo si perdono tutti i vantaggi, costituiti dall'eliminazione di tutta una serie di errori a tempo di compilazione nonchè della "documentazione interna al programma" illustrati in questi articoli.

 

Conclusioni
Questo articolo conclude una serie finalizzata ad introdurre alcuni concetti fondamentali della programmazione ad oggetti in Java attraverso l'analisi di uno schema di uso frequente che abbiamo chiamato schema Kernel-Modulo.

 

Bibliografia
[1] Bruce Eckel - "Thinking in Java", Edizione Italiana, Apogeo, 2002. http://www.mindview.net/Books
[2] Cay S. Horstmann e Gary Cornell - "Java 2: i fondamenti", McGraw-Hill, 2001.
[3] Cay S. Horstmann e Gary Cornell - "Java 2: tecniche avanzate", McGraw-Hill, 2000 .
[4] Meilir Page-Jones - "Progettazione a oggetti con UML", Apogeo, 2002.

Matteo Baldoni è ricercatore in informatica presso il Dipartimento di Informatica dell'Università degli Studi di Torino.

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it