Ce petit document est fait pour que vous ne vous plantiez pas durant le prochain examen de Java. Il vise à vous faire comprendre les mécanismes parfois un peu flous de la dérivation et du polymorphisme.
Si vous n'êtes pas à l'aise avec ces notions, aucun problème, ce document est fait pour vous !
Si par contre, vous pensez maitriser ces concepts, nous allons tout de même commencer par le petit problème sur lequel tout le monde se plante (même moi la première fois).
Soit deux classes Java A et B, B dérivant de A, avec pleins de fonctions compliquées :
class A { int f(A a) { return 1; } } class B extends A { int f(A a) { return 2; } int f(B b) { return 3; } }
Il faut maintenant calculer le retour de chacun de ces appels :
public static void main(String[] astrArgs) { A a = new A(); A ab = new B(); B b = new B(); // Partie a System.out.println( a.f(a) ); System.out.println( a.f(ab) ); System.out.println( a.f(b) ); // Partie ab System.out.println( ab.f(a) ); System.out.println( ab.f(ab) ); System.out.println( ab.f(b) ); // Partie b System.out.println( b.f(a) ); System.out.println( b.f(ab) ); System.out.println( b.f(b) ); }
Vous pouvez ensuite comparez vos résultats aves les résultats suivants :
-- Partie a -- 1 1 1 -- Partie ab -- 2 2 2 -- Partie b -- 2 2 3
Si vous ne trouvez pas la même chose pour les parties a et b ? C'est qu'il faut que vous révisiez votre cours de Java ! Ou mieux... Regardez ce document jusqu'au bout !
Si vous avez seulement fait une erreur sur le troisième calcul de la partie ab, alors vous êtes comme 90% des autres, vous avez compris la dérivation et le polymorphisme, mais pas ses subtilités.
Une classe est un type. C'est un peu simpliste mais il faut commencer par le début. Le fait que ce soit un type signifie donc qu'elle n'a pas réellement d'existence en dehors de la compilation, ce n'est qu'une notion utilisée par le programmeur pour se simplifier la tâche (et ne pas sombrer dans la folie au premier projet).
Donc pour résoudre notre petit problème, nous allons nous tourner vers le compilateur et essayer de comprendre comment il fonctionne pour nous faire ces fameuses classes qui au final, n'existent plus.
Commençons par le début, le compilateur commence toujours par compiler les classes avant de compiler le code à exécuter.
class A { int f(A a) { return 1; } }
En voyant cela, le compilateur va se créer un objet classe dans lequel il va enregistrer toutes les informations de cette classes (attributs, classes internes, méthodes). Ici ce sont les méthodes qui nous intéressent.
Pour stocker les méthodes, le compilateur va créer une table afin
d'associer au nom de la fonction un pointeur sur son code. Rappelons
qu'un nom de fonction est composé du nom qu'on lui donne plus les types
de ces arguments. Dans notre cas, le compilateur crée donc une table
associant à f(A)
le pointeur vers le code de la méthode définie.
f(A) | { return 1; } |
---|
class B extends A { int f(A a) { return 2; } int f(B b) { return 3; } }
Ensuite pour créer la table de B, rien de plus simple, il suffit de prendre la table de A (puisque B dérive de A) et de la recopier. Il suffit ensuite de modifier cette table en fonction des méthodes qui seraient définies ou redéfinies dans cette classe.
f(A) | { return 2; } // Redéfinition, on modifie donc le pointeur |
---|---|
f(B) | { return 3; } // Surcharge, c'est donc une nouvelle méthode |
Une fois les tables de nos classes connues, on peut passer à l'analyse du code à exécuter.
Pour compiler un appel de méthode, il suffit de regarder le type déclarés des objets entrant en jeu dans cet appel.
ab.f(a)
Les types mis en œuvre ici nous renseignent sur le fait que nous allons faire un appel à la méthode A.f(A)
(ab et a étant tous les deux de type A).
Le compilateur recherche alors la méthode de la classe A la plus à même de répondre à ce besoin : A.f(A)
! Gagné, même pas besoin de s'embéter. f(A)
est donc sélectionné pour l'exécution :
ab.Table["f(A)"](a)
On utilise la même technique pour ab.f(ab)
que l'on replacera par ab.Table["f(A)"](ab)
.
Analysons maintenant le code qui pose problème...
ab.f(b)
D'après les types, le compilateur recherche la méthode A.f(B)
. Cette méthode n'est pas disponible dans la classe A, mais il existe une méthode capable de répondre à nos besoins, la méthode A.f(A)
.
Cette méthode peut être appelée puisque notre classe B dérive de la
classe A, elle est donc tout à fait utilisable s'il n'y a pas de
méthode A.f(B)
(et en l'occurrence il n'y en a pas).
On pourrait se demander pourquoi le compilateur ne va pas chercher dans les méthodes de B ? Parce que c'est un compilateur... Et un compilateur ne peut pas deviner plus que les informations que vous lui donnez, à savoir les déclarations de types. Donc pour lui, ab est un A, et restera toujours un A.
Le code est donc transformé ainsi :
ab.Table["f(A)"](b)
Voilà, notre programme est maintenant dans la moulinette, près à être exécuté.
Reprenons ce que le compilateur nous a demandé d'exécuter pour la partie ab :
ab.Table["f(A)"](a) ab.Table["f(A)"](ab) ab.Table["f(A)"](b)
Nous allons donc demander à chacun des objets d'aller fouiller dans
sa table pour nous trouver la fonction qu'il faut... « Eh oh ! Objet ab ! Veux-tu bien m'exécuter la méthode f(A)
situé dans ta table ? »
ab regarde alors dans sa table... Mais ab est un objet de classe B à cet instant, n'oubliez pas, son type ne compte plus, maintenant c'est un objet, seulement un objet... Il recherche donc dans sa table et voici ce qu'il voit :
f(A) | { return 2; } |
---|---|
f(B) | { return 3; } |
Eh oui, étant finalement un objet de type B, il contient en interne
ce fameux attribut « table des méthodes » dont il va se servir pour
trouver le code de ses méthodes. En lui demandant d'exécuter le code de
f(A)
, il va donc exécuter { return 2; }
.
Il fera de même pour la deuxième instruction, et fera encore de même pour la troisième...