La signature d'une méthode inclut-elle le type de retour en Java?

1. Vue d'ensemble

La signature de méthode n'est qu'un sous-ensemble de toute la définition de méthode en Java. Ainsi, l'anatomie exacte de la signature peut prêter à confusion.

Dans ce didacticiel, nous allons apprendre les éléments de la signature de méthode et ses implications dans la programmation Java.

2. Signature de la méthode

Les méthodes Java prennent en charge la surcharge, ce qui signifie que plusieurs méthodes portant le même nom peuvent être définies dans la même classe ou hiérarchie de classes. Par conséquent, le compilateur doit être capable de lier statiquement la méthode à laquelle le code client fait référence. Pour cette raison, la signature de méthode identifie chaque méthode de manière unique .

Selon Oracle, la signature de méthode comprend le nom et les types de paramètres . Par conséquent, tous les autres éléments de la déclaration de la méthode, tels que les modificateurs, le type de retour, les noms de paramètres, la liste d'exceptions et le corps ne font pas partie de la signature.

Examinons de plus près la surcharge de méthode et son lien avec les signatures de méthode.

3. Erreurs de surcharge

Considérons le code suivant :

public void print() { System.out.println("Signature is: print()"); } public void print(int parameter) { System.out.println("Signature is: print(int)"); }

Comme nous pouvons le voir, le code se compile car les méthodes ont différentes listes de types de paramètres. En effet, le compilateur peut lier de manière déterministe tout appel à l'un ou à l'autre.

Testons maintenant s'il est légal de surcharger en ajoutant la méthode suivante:

public int print() { System.out.println("Signature is: print()"); return 0; }

Lorsque nous compilons, nous obtenons une erreur «la méthode est déjà définie dans la classe». Cela prouve que le type de retour de méthode ne fait pas partie de la signature de la méthode .

Essayons la même chose avec les modificateurs:

private final void print() { System.out.println("Signature is: print()"); }

Nous voyons toujours la même erreur «la méthode est déjà définie dans la classe». Par conséquent, la signature de la méthode ne dépend pas des modificateurs .

La surcharge en modifiant les exceptions levées peut être testée en ajoutant:

public void print() throws IllegalStateException { System.out.println("Signature is: print()"); throw new IllegalStateException(); }

Encore une fois, nous voyons l'erreur «la méthode est déjà définie dans la classe», indiquant que la déclaration throw ne peut pas faire partie de la signature .

La dernière chose que nous pouvons tester est de savoir si la modification des noms de paramètres autorise la surcharge. Ajoutons la méthode suivante:

public void print(int anotherParameter) { System.out.println("Signature is: print(int)"); }

Comme prévu, nous obtenons la même erreur de compilation. Cela signifie que les noms de paramètres n'influencent pas la signature de la méthode .

3. Génériques et effacement de type

Avec les paramètres génériques , l'effacement de type modifie la signature effective . En effet, cela peut provoquer une collision avec une autre méthode qui utilise la limite supérieure du type générique au lieu du jeton générique.

Considérons le code suivant:

public class OverloadingErrors { public void printElement(T t) { System.out.println("Signature is: printElement(T)"); } public void printElement(Serializable o) { System.out.println("Signature is: printElement(Serializable)"); } }

Même si les signatures semblent différentes, le compilateur ne peut pas lier statiquement la méthode correcte après l'effacement du type.

Nous pouvons voir le compilateur remplacer T par la limite supérieure, Serializable, en raison de l'effacement du type. Ainsi, il entre en conflit avec la méthode utilisant explicitement Serializable .

Nous verrions le même résultat avec le type de base Object lorsque le type générique n'a pas de limite.

4. Listes de paramètres et polymorphisme

La signature de la méthode prend en compte les types exacts. Cela signifie que nous pouvons surcharger une méthode dont le type de paramètre est une sous-classe ou une superclasse.

Cependant, nous devons prêter une attention particulière car la liaison statique tentera de correspondre en utilisant le polymorphisme, l'auto-boxing et la promotion de type .

Jetons un coup d'œil au code suivant:

public Number sum(Integer term1, Integer term2) { System.out.println("Adding integers"); return term1 + term2; } public Number sum(Number term1, Number term2) { System.out.println("Adding numbers"); return term1.doubleValue() + term2.doubleValue(); } public Number sum(Object term1, Object term2) { System.out.println("Adding objects"); return term1.hashCode() + term2.hashCode(); }

Le code ci-dessus est parfaitement légal et compilera. Une confusion peut survenir lors de l'appel de ces méthodes car nous devons non seulement connaître la signature de méthode exacte que nous appelons, mais aussi comment Java se lie statiquement en fonction des valeurs réelles.

Explorons quelques appels de méthode qui finissent par être liés à la somme (Integer, Integer) :

StaticBinding obj = new StaticBinding(); obj.sum(Integer.valueOf(2), Integer.valueOf(3)); obj.sum(2, 3); obj.sum(2, 0x1);

Pour le premier appel, nous avons les types de paramètres exacts Integer, Integer. Lors du deuxième appel, Java convertira automatiquement int en Integer pour nous . Enfin, Java transformera la valeur d'octet 0x1 en int au moyen de la promotion de type et la boxera automatiquement en Integer.

De même, nous avons les appels suivants qui se lient à sum (Number, Number) :

obj.sum(2.0d, 3.0d); obj.sum(Float.valueOf(2), Float.valueOf(3));

Lors du premier appel, nous avons des valeurs doubles qui sont automatiquement encadrées à Double. Et puis, au moyen du polymorphisme, Double correspond à Number. De la même manière, Float correspond à Number pour le deuxième appel.

Let's observe the fact that both Float and Double inherit from Number and Object. However, the default binding is to Number. This is due to the fact that Java will automatically match to the nearest super-types that match a method signature.

Now let's consider the following method call:

obj.sum(2, "John");

In this example, we have an int to Integer auto-box for the first parameter. However, there is no sum(Integer, String) overload for this method name. Consequentially, Java will run through all the parameter super-types to cast from the nearest parent to Object until it finds a match. In this case, it binds to sum(Object, Object).

To change the default binding, we can use explicit parameter casting as follows:

obj.sum((Object) 2, (Object) 3); obj.sum((Number) 2, (Number) 3);

5. Vararg Parameters

Now let's turn our attention over to how varargs impact the method's effective signature and static binding.

Here we have an overloaded method using varargs:

public Number sum(Object term1, Object term2) { System.out.println("Adding objects"); return term1.hashCode() + term2.hashCode(); } public Number sum(Object term1, Object... term2) { System.out.println("Adding variable arguments: " + term2.length); int result = term1.hashCode(); for (Object o : term2) { result += o.hashCode(); } return result; }

So what are the effective signatures of the methods? We've already seen that sum(Object, Object) is the signature for the first. Variable arguments are essentially arrays, so the effective signature for the second after compilation is sum(Object, Object[]).

A tricky question is how can we choose the method binding when we have just two parameters?

Let's consider the following calls:

obj.sum(new Object(), new Object()); obj.sum(new Object(), new Object(), new Object()); obj.sum(new Object(), new Object[]{new Object()});

Obviously, the first call will bind to sum(Object, Object) and the second to sum(Object, Object[]). To force Java to call the second method with two objects, we must wrap it in an array as in the third call.

The last thing to note here is that declaring the following method will clash with the vararg version:

public Number sum(Object term1, Object[] term2) { // ... }

6. Conclusion

In this tutorial, we learned that the method signatures are comprised of the name and the parameter types' list. The modifiers, return type, parameter names, and exception list cannot differentiate between overloaded methods and, thus, are not part of the signature.

We've also looked at how type erasure and varargs hide the effective method signature and how we can override Java's static method binding.

Comme d'habitude, tous les exemples de code présentés dans cet article sont disponibles à l'adresse over sur GitHub.