UKOnline

Surcharge de méthode

Lorsqu'on définit une classe, on ne peut pas, en règle générale, y définir plus d'une méthode avec le même nom, sauf dans certains cas très précis. Ceci est précisément le sujet de cette section.

Rappelez-vous du chapitre précédent, l'objet System.out est de type PrintStream. Si on consulte la documentation de cette classe, on peut voir qu'elle contient plusieurs versions de la méthode println. On y retrouve en particulier une qui prend un int en paramètre, une autre qui prend un boolean, etc.

Pour comprendre l'intérêt de pouvoir définir plusieurs méthodes avec le même nom, on va partir de l'exemple suivant qui utilise cette méthode println avec des paramètres réels de différents types :

L'exécution de ce programme affiche sur la sortie standard :

5
true
Sun Oct 26 00:00:00 CEST 2008

Méthode surchargée

Lorsqu'on définit une classe, on peut y définir plusieurs méthodes qui portent le même nom. La seule contrainte est que la liste des types des paramètres formels doit être différente. Ce mécanisme est appelé surcharge de méthode. Il n'y a aucune contrainte sur les différents modificateurs ou sur la valeur de retour et on peut également appliquer la surcharge aux constructeurs.

Poursuivons l'écriture de la classe Die. Ce qu'on aimerait, c'est pouvoir créer un dé de deux manières différentes : soit on spécifie un nombre de faces ou alors on ne dit rien et on obtient un dé standard à six faces. Voici donc les deux constructeurs de la classe :

Lors de la création d'un nouvel objet de la classe Die, le bon constructeur sera appelé en fonction des types de la liste des paramètres réels. Prenons par exemple les deux instructions suivantes :

L'exécution de la première instruction va invoquer le premier constructeur puisque la liste des paramètres est vide. Par contre, en ce qui concerne la seconde instruction, il y a un paramètre réel de type int (le littéral 12) et ce sera donc le second constructeur qui sera exécuté.

Voyons un autre exemple. On désire définir une classe Die avec deux constructeurs. Le premier permet de créer un dé en précisant le nombre de faces qu'on veut. Et le second permet de créer un dé à six faces tout en précisant la valeur de la face visible :

Cette classe ne va pas compiler. En effet, les deux méthodes ont la même liste de types de paramètres formels et il ne s'agit donc pas de méthodes surchargées :

Die.java:12: Die(int) is already defined in Die
	public Die (int visible)
	       ^
1 error

Résolution de surcharge

Comment Java fait-il pour déterminer quelle version d'une méthode surchargée doit être appelée ? Ce choix est fait lors de la compilation du programme, lors d'une phase appelée résolution de surcharge. Il est fait sur base des types des paramètres réels utilisés.

Le compilateur va va construire la liste des types des paramètres réels et recherche ensuite une méthode qui corresponde à cette liste. S'il ne trouve pas une méthode correspondant, le compilateur va tenter en convertissant les paramètres réels. Les conversions doivent être sans perte d'information, c'est-à-dire des conversions explicites comme vues au chapitre 2. Finalement, si le compilateur ne trouve aucune méthode qui correspond, une erreur de type « The method XXX in the type YYY is not applicable for the arguments (ZZZ) » sera générée.

On va commencer par voir la procédure appliquée par le compilateur sur base d'un exemple avant d'en venir aux règles précises. Voici une classe Overloading qui contient une méthode surchargée de nom test qui est définie en quatre versions :

Toutes les versions de la méthode test possèdent bien des listes de types de paramètres formels différents et il s'agit donc bien d'une surcharge de méthode autorisée. Pour rappel, il s'agit de la seule condition à respecter, les types de retour ne doivent pas être les mêmes. Voyons maintenant comment le compilateur choisit la méthode à invoquer sur base de l'exemple suivant :

Correspondance exacte

Commençons avec le premier appel :

Le premier paramètre réel est de type byte et le second de type int. Le compilateur recherche donc une méthode test qui prend deux paramètres et dont la liste des types des paramètres formels est (byte, int). Il y a une correspondance parfaite et c'est donc la méthode public double test (byte a, int b) qui sera appelée. L'exécution de cette instruction affiche donc D 102.0 sur la sortie standard.

Conversion implicite

Voyons maintenant le second appel :

La liste des types des paramètres réels est (short, int). Il n'y a pas de correspondance parfaite, néanmoins, si le premier paramètre est converti en int, alors la méthode appelée sera public int test (int a, int b), celle dont la liste des types des paramètres formels est (int, int). L'exécution de cette instruction affiche donc I 133 sur la sortie standard. La conversion de short vers int est bien une conversion sans perte d'information et se fait donc implicitement.

Aucune correspondance

Passons maintenant au troisième appel :

La liste des types des paramètres réels est (long, int) et il n'y a aucune correspondance possible. Peu importe les conversions sans perte d'information qu'on peut appliquer, aucune version de la méthode surchargée ne correspond. Cette instruction génère donc une erreur de compilation :

TestOverloading.java:13: cannot find symbol
symbol  : method test(long,int)
location: class Overloading
		System.out.println (o.test (l, i));
		                     ^
1 error

Appel ambigu

Voyons enfin le dernier appel :

La liste des types des paramètres réels est (int, short). On se retrouve dans une situation où deux solutions sont possibles. Soit le premier paramètre est converti de int à long et c'est la méthode \code{public char test (long a, short b)} qui est appelée. Soit, le second paramètre est converti de short à int et c'est la méthode public int test (int a, int b) qui est appelée.

Le compilateur ne sachant pas quelle méthode choisir, il ne va pas en choisir une au hasard, mais plutôt générer une erreur de compilation :

TestOverloading.java:14: reference to test is ambiguous, both method test(int,int) in Overloading and method test(long,short) in Overloading match
		System.out.println (o.test (i, s));
		                     ^
1 error

Règle de résolution de surcharge

Maintenant qu'on a vu les différents cas qui peuvent se produire lors de la résolution de surcharge, on va voir précisément les différentes étapes par lesquelles passe le compilateur. On va également voir comment l'auto-boxing/unboxing des types primitifs et les méthodes avec un nombre variable de paramètres sont gérés.

Méthode candidate

La première chose que la compilateur fait lorsqu'il tombe sur un appel de méthode, c'est identifier une série de méthodes dites candidates. Il s'agit des méthodes qui satisfont les conditions suivantes :

  1. elle possède le même nom que dans l'appel ;
  2. la méthode est visible ;
  3. son nombre de paramètres formels est plus petit ou égal au nombre de paramètres réels ;
  4. si la méthode a un paramètre formel de longueur variable et possède $n$ paramètres formels en tout, le nombre de paramètres réels doit être plus grand ou égal à $n - 1$ ;
  5. si la méthode n'a pas de paramètre formel de longueur variable et possède $n$ paramètres formels en tout, le nombre de paramètres réels doit être exactement égal à $n$.

Si aucune méthode candidate n'est trouvée, le compilateur génère une erreur de compilation. Sinon, il passe à la phase 1.

Phase 1 : Conversion implicite des paramètres réels

Lors de cette première phase, les différentes méthodes candidates vont être analysées. Notons par $m$ une telle méthode candidate. Le compilateur construit d'abord la liste des types des paramètres formels de $m$ qu'on va noter par $(f_1, ..., f_n)$. La méthode $m$ est candidate par conversion si les types des paramètre réels peuvent être convertis vers les types des paramètres formels. Si on note la liste des types des paramètres réels par $(r_1, ..., r_n)$, il faut que, pour tout $i$ compris entre $1$ et $n$, $r_i$ puisse être converti vers $f_i$.

Pour rappel, les paramètres réels peuvent être convertis par conversion sans perte d'information ou par boxing/unboxing éventuellement suivi par une conversion sans perte d'information.

Si aucune méthode n'a été trouvée au terme de cette phase, le compilateur passe à la phase 2. Sinon, le compilateur choisi la méthode la plus spécifique parmi les méthodes candidates par conversion.

Phase 2 : Liste de paramètres de longueur variable

Le compilateur part maintenant à la recherche de méthodes qui ont un nombre de paramètres variable. Le compilateur va analyser les méthodes $m$ qui ont $n$ paramètres formels, et dont le dernier paramètre est de longueur variable. La méthode $m$ est candidate avec un paramètre variable si les types des paramètres réels peuvent être convertis vers les types des paramètres formels, notés $(f_1, ..., f_n)$. Si on note par $(r_1, ..., r_k)$ les types des paramètres réels, avec $k$ plus grand ou égal à $n - 1$, il faut que, pour tout $i$ compris entre $1$ et $n - 1$, $r_i$ puisse être converti vers $f_i$ et il faut que, pour tout $i$ compris entre $n$ et $k$, $r_i$ puisse être converti vers $f_n$, ce $n$e paramètre étant celui de longueur variable.

Si aucune méthode n'a été trouvée au terme de cette phase, le compilateur génère une erreur de compilation. Sinon, il choisi la méthode la plus spécifique parmi les méthodes candidates avec un paramètre variable.

Méthode la plus spécifique

Lorsque le compilateur trouve plusieurs méthodes candidates au terme de la phase 1 ou 2, il doit choisir laquelle sera appelée. Il choisit la méthode la plus spécifique. Intuitivement, une méthode $A$ est plus spécifique qu'une méthode $B$ si tous les appels que $A$ sait gérer, $B$ peut également les gérer sans erreur de compilation.

Une méthode $A$ dont les types des paramètres formels sont $(a_1, ..., a_n)$ est plus spécifique qu'une méthode $B$ dont les types des paramètres formels sont $(b_1, ..., b_n)$ si, pour tout $i$ compris entre $1$ et $n$, $b_i$ peut être converti sans perte d'information ou par un boxing/unboxing éventuellement suivi d'une conversion sans perte d'information en $a_i$.

S'il n'y a qu'une seule méthode la plus spécifique trouvée, elle sera choisie par le compilateur pour être exécutée. Sinon, l'appel est ambigu et le compilateur va générer une erreur de compilation du type « The method XXX is ambiguous for the type YYY ».

Un exemple

Voyons encore un exemple avec des méthodes avec un nombre variable de paramètres et avec de l'auto boxing/unboxing. Soient les signatures de méthode suivantes :

Prenons par exemple l'appel sum (2, 3). Le compilateur Java identifie d'abord les méthodes candidates en comptant le nombre de paramètres formels. Les méthodes candidates sont les méthodes 1, 2, 3 et 4. La cinquième méthode possède trois paramètres formels et le nombre de paramètres est fixe, elle est donc rejetée.

Lors de la première phase, le compilateur va regarder les types des paramètres. La liste des types des paramètres réels est (int, int). Le compilateur va maintenant analyser si ces types peuvent être convertis pour correspondre aux méthodes. Les méthodes avec un paramètre variable sont ignorées dans cette phase et il reste donc les méthodes 2, 3 et 4. Parmi ces trois méthodes, la seconde va être rejetée car la liste des types (int, int) ne peut être convertie en (int, String). Il reste donc les méthodes 3 et 4.

Le compilateur doit maintenant choisir la plus spécifique entre ceux deux méthodes. Puisqu'il est possible de convertir de int à int et de int à long, la quatrième méthode est donc plus spécifique que la troisième et sera donc choisie par le compilateur.