Dispatch Dinamico dei Metodi in Java
- L'override dei metodi consente a una sottoclasse di fornire un'implementazione specifica di un metodo già definito nella sua superclasse.
- Il dispatch dinamico dei metodi è il meccanismo che risolve le chiamate ai metodi sovrascritti a tempo di esecuzione, permettendo il polimorfismo a tempo di esecuzione.
- L'override dei metodi è fondamentale per il polimorfismo a tempo di esecuzione, consentendo a una classe generica di specificare metodi comuni a tutte le sue derivate, con la possibilità per le sottoclassi di definire implementazioni specifiche.
- Il polimorfismo a tempo di esecuzione consente a librerie di codice esistenti di chiamare metodi su istanze di nuove classi senza necessità di ricompilare, mantenendo un'interfaccia astratta pulita.
- L'override dei metodi è un potente strumento per il riutilizzo del codice e la robustezza nella programmazione orientata agli oggetti.
Dispatch Dinamico dei Metodi
Mentre gli esempi nella lezione precedente dimostrano la meccanica dell'override dei metodi, non ne mostrano la potenza.
In effetti, se non ci fosse altro nell'override dei metodi se non una convenzione di nomi, sarebbe, nella migliore delle ipotesi, una curiosità interessante, ma di poco valore reale.
Tuttavia, non è così. L'override dei metodi costituisce la base di uno dei concetti più potenti di Java: dispatch dinamico dei metodi. Il dispatch dinamico dei metodi è il meccanismo mediante il quale una chiamata a un metodo sovrascritto viene risolta a tempo di esecuzione, anziché a tempo di compilazione. Il dispatch dinamico dei metodi è importante perché è così che Java implementa il polimorfismo a tempo di esecuzione.
Iniziamo ribadendo un principio importante: una variabile di riferimento a una superclasse può fare riferimento a un oggetto di una sottoclasse.
Java utilizza questo fatto per risolvere le chiamate ai metodi sovrascritti a tempo di esecuzione. Ecco come: quando un metodo sovrascritto viene chiamato attraverso un riferimento alla superclasse, Java determina quale versione di quel metodo eseguire in base al tipo dell'oggetto a cui si fa riferimento al momento in cui avviene la chiamata. Pertanto, questa determinazione viene effettuata a tempo di esecuzione.
Quando si fa riferimento a diversi tipi di oggetti, vengono chiamate diverse versioni di un metodo sovrascritto. In altre parole, è il tipo dell'oggetto a cui si fa riferimento (e non il tipo della variabile di riferimento) a determinare quale versione di un metodo sovrascritto verrà eseguita. Pertanto, se una superclasse contiene un metodo che viene sovrascritto da una sottoclasse, allora quando si fa riferimento a diversi tipi di oggetti tramite una variabile di riferimento alla superclasse, vengono eseguite diverse versioni del metodo.
Ecco un esempio che illustra il dispatch dinamico dei metodi:
// Dispatch Dinamico dei Metodi
class A {
void chiamaMe() {
System.out.println("Dentro il metodo chiamaMe di A");
}
}
class B estende A {
// override di chiamaMe()
void chiamaMe() {
System.out.println("Dentro il metodo chiamaMe di B");
}
}
class C estende A {
// override di chiamaMe()
void chiamaMe() {
System.out.println("Dentro il metodo chiamaMe di C");
}
}
class Dispatch {
public static void main(String[] args) {
A a = new A(); // oggetto di tipo A
B b = new B(); // oggetto di tipo B
C c = new C(); // oggetto di tipo C
A r; // ottenere un riferimento di tipo A
r = a; // r fa riferimento a un oggetto A
r.chiamaMe(); // chiama la versione di A di chiamaMe
r = b; // r fa riferimento a un oggetto B
r.chiamaMe(); // chiama la versione di B di chiamaMe
r = c; // r fa riferimento a un oggetto C
r.chiamaMe(); // chiama la versione di C di chiamaMe
}
}
L'output del programma è il seguente:
Dentro il metodo chiamaMe di A
Dentro il metodo chiamaMe di B
Dentro il metodo chiamaMe di C
Questo programma crea una superclasse chiamata A
e due sottoclassi di essa, chiamate B
e C
. Le sottoclassi B
e C
sovrascrivono chiamaMe()
dichiarato in A
.
All'interno del metodo main()
, vengono dichiarati oggetti di tipo A
, B
e C
. Inoltre, viene dichiarato un riferimento di tipo A
, chiamato r
.
Il programma poi assegna successivamente a r
un riferimento a ciascun tipo di oggetto e usa quel riferimento per invocare chiamaMe()
. Come mostra l'output, la versione di chiamaMe()
eseguita è determinata dal tipo dell'oggetto a cui si fa riferimento al momento della chiamata. Se fosse stata determinata dal tipo della variabile di riferimento, r
, si vedrebbero tre chiamate al metodo chiamaMe()
di A
.
Metodi virtuali nei linguaggi OOP
I lettori familiari con C++ o C# riconosceranno che i metodi sovrascritti in Java sono simili alle funzioni virtuali in quei linguaggi.
Perché effettuare l'override dei metodi?
Come affermato in precedenza, i metodi sovrascritti consentono a Java di supportare il polimorfismo a tempo di esecuzione.
Il polimorfismo è essenziale per la programmazione orientata agli oggetti per un motivo: consente a una classe generica di specificare metodi che saranno comuni a tutte le sue derivate, consentendo alle sottoclassi di definire l'implementazione specifica di alcuni o tutti quei metodi. I metodi sovrascritti sono un altro modo con cui Java implementa l'aspetto del polimorfismo: una sola interfaccia, più implementazioni.
Parte della chiave per applicare con successo il polimorfismo consiste nel comprendere che le superclassi e le sottoclassi formano una gerarchia che si sposta da una specializzazione minore a una maggiore. Se utilizzata correttamente, la superclasse fornisce tutti gli elementi che una sottoclasse può usare direttamente. Essa definisce anche quei metodi che la classe derivata deve implementare da sola. Questo consente alla sottoclasse la flessibilità di definire i propri metodi, pur mantenendo un'interfaccia coerente. Quindi, combinando l'ereditarietà con i metodi sovrascritti, una superclasse può definire la forma generale dei metodi che saranno usati da tutte le sue sottoclassi.
Il polimorfismo a tempo di esecuzione è uno dei meccanismi più potenti che il design orientato agli oggetti offre in termini di riutilizzo del codice e robustezza. La possibilità di librerie di codice esistenti di chiamare metodi su istanze di nuove classi senza ricompilare, pur mantenendo un'interfaccia astratta pulita, è uno strumento estremamente potente.
Applicare l'override dei metodi
Analizziamo un esempio più pratico che utilizza l'override dei metodi.
Il seguente programma crea una superclasse chiamata Figura
che memorizza le dimensioni di un oggetto bidimensionale. Definisce anche un metodo chiamato area()
che calcola l'area di un oggetto. Il programma deriva due sottoclassi da Figura
. La prima è Rettangolo
e la seconda è Triangolo
. Ciascuna di queste sottoclassi sovrascrive area()
in modo che restituisca l'area rispettivamente di un rettangolo e di un triangolo.
// Uso del polimorfismo a tempo di esecuzione.
class Figura {
double dim1;
double dim2;
Figura(double a, double b) {
dim1 = a;
dim2 = b;
}
double area() {
System.out.println("Area per Figura non definita.");
return 0;
}
}
class Rettangolo estende Figura {
Rettangolo(double a, double b) {
super(a, b);
}
// override di area per rettangolo
double area() {
System.out.println("Dentro area per Rettangolo.");
return dim1 * dim2;
}
}
class Triangolo estende Figura {
Triangolo(double a, double b) {
super(a, b);
}
// override di area per triangolo rettangolo
double area() {
System.out.println("Dentro area per Triangolo.");
return dim1 * dim2 / 2;
}
}
class TrovaAree {
public static void main(String[] args) {
Figura f = new Figura(10, 10);
Rettangolo r = new Rettangolo(9, 5);
Triangolo t = new Triangolo(10, 8);
Figura rifFigura;
rifFigura = r;
System.out.println("Area è " + rifFigura.area());
rifFigura = t;
System.out.println("Area è " + rifFigura.area());
rifFigura = f;
System.out.println("Area è " + rifFigura.area());
}
}
L'output del programma è il seguente:
Dentro area per Rettangolo.
Area è 45
Dentro area per Triangolo.
Area è 40
Area per Figura non definita.
Area è 0
Attraverso i meccanismi combinati di ereditarietà e polimorfismo a tempo di esecuzione, è possibile definire un'interfaccia coerente che viene usata da diversi tipi di oggetti, differenti ma correlati. In questo caso, se un oggetto è derivato da Figura
, allora la sua area può essere ottenuta chiamando area()
. L'interfaccia di questa operazione è la stessa indipendentemente dal tipo di figura in uso.