Comment fonctionne la surcharge de méthodes en Java?

Maintenant que vous êtes familier avec les règles pour déclarer et utiliser des méthodes, il est temps d’examiner la création de méthodes portant le même nom dans une même classe. La surcharge de méthode se produit lorsque des méthodes dans la même classe ont le même nom mais des signatures différentes, ce qui signifie qu’elles utilisent des listes de paramètres différentes. La surcharge diffère de la redéfinition, que vous découvrirez dans le Chapitre 6.)

Nous avons déjà montré comment appeler des méthodes surchargées depuis un certain temps. System.out.println() et les méthodes append() de StringBuilder fournissent de nombreuses versions surchargées, vous permettant ainsi de leur passer à peu près n’importe quoi sans avoir à y réfléchir. Dans ces deux exemples, seul le type du paramètre change. La surcharge permet également d’utiliser différents nombres de paramètres.

Tout sauf le nom de la méthode peut varier pour surcharger des méthodes. Cela signifie qu’il peut y avoir différents modificateurs d’accès, spécificateurs optionnels (comme static), types de retour et listes d’exceptions.

L’exemple suivant montre cinq versions surchargées de la méthode voler() :

public class Faucon {
    public void voler(int nbKilometres) {}
    public void voler(short nbPieds) {}
    public boolean voler() { return false; }
    void voler(int nbKilometres, short nbPieds) {}
    public void voler(short nbPieds, int nbKilometres) throws Exception {}
}

Comme vous pouvez le voir, nous pouvons surcharger en modifiant n’importe quoi dans la liste des paramètres. Nous pouvons avoir un type différent, plus de types, ou les mêmes types dans un ordre différent. Notez également que le type de retour, le modificateur d’accès et la liste d’exceptions sont sans importance pour la surcharge. Seuls le nom de la méthode et la liste des paramètres comptent.

Examinons maintenant un exemple qui n’est pas une surcharge valide :

public class Aigle {
    public void voler(int nbKilometres) {}
    public int voler(int nbKilometres) { return 1; } // NE COMPILE PAS
}

Cette méthode ne compile pas car elle ne diffère de l’originale que par le type de retour. Les signatures des méthodes sont les mêmes, donc ce sont des méthodes dupliquées du point de vue de Java.

Pourquoi celles-ci ne compilent-elles pas ?

public class Faucon {
    public void voler(int nbKilometres) {}
    public static void voler(int nbKilometres) {} // NE COMPILE PAS
    public void voler(int nbKilometres) {} // NE COMPILE PAS
}

Encore une fois, les signatures de ces trois méthodes sont les mêmes. Vous ne pouvez pas déclarer de méthodes dans la même classe où la seule différence est qu’une est une méthode d’instance et l’autre est une méthode static. Vous ne pouvez pas non plus avoir deux méthodes avec des listes de paramètres ayant les mêmes types de variables et dans le même ordre. Comme nous l’avons mentionné précédemment, les noms des paramètres dans la liste n’ont pas d’importance lors de la détermination de la signature de la méthode.

Appeler des méthodes surchargées est facile. Vous écrivez simplement le code, et Java appelle la bonne méthode. Par exemple, examinez ces deux méthodes :

public class Colombe {
    public void voler(int nbKilometres) {
        System.out.println("int");
    }
    public void voler(short nbPieds) {
        System.out.println("short");
    }
}

L’appel voler((short) 1) affiche short. Java recherche les types correspondants et appelle la méthode appropriée. Bien sûr, cela peut être plus compliqué.

Maintenant que vous connaissez les bases de la surcharge, examinons des scénarios plus complexes que vous pourriez rencontrer.

Types de Référence

Étant donné la règle selon laquelle Java choisit la version la plus spécifique d’une méthode qu’il peut, que pensez-vous que ce code affiche ?

public class Pelican {
    public void voler(String s) {
        System.out.print("chaîne");
    }
    public void voler(Object o) {
        System.out.print("objet");
    }
    public static void main(String[] args) {
        var p = new Pelican();
        p.voler("test");
        System.out.print("-");
        p.voler(56);
    }
}

La réponse est chaîne-objet. Le premier appel passe une chaîne et trouve une correspondance directe. Il n’y a aucune raison d’utiliser la version Object quand il y a une belle liste de paramètres String qui attend d’être appelée. Le deuxième appel cherche une liste de paramètres int. Quand il n’en trouve pas, il effectue un autoboxing vers Integer. Comme il ne trouve toujours pas de correspondance, il passe à la version Object.

Essayons un autre exemple. Que fait celui-ci ?

import java.time.*;
import java.util.*;
public class Perroquet {
    public static void imprimer(List<Integer> i) {
        System.out.print("I");
    }
    public static void imprimer(CharSequence c) {
        System.out.print("C");
    }
    public static void imprimer(Object o) {
        System.out.print("O");
    }
    public static void main(String[] args){
        imprimer("abc");
        imprimer(Arrays.asList(3));
        imprimer(LocalDate.of(2019, Month.JULY, 4));
    }
}

La réponse est CIO. Le premier appel à imprimer() passe une String. Comme vous avez appris au Chapitre 4, String et StringBuilder implémentent l’interface CharSequence. Vous avez également appris que Arrays.asList() peut être utilisé pour créer un objet List<Integer>, ce qui explique la deuxième sortie. Le dernier appel à imprimer() passe un LocalDate. C’est une classe que vous ne connaissez peut-être pas, mais ce n’est pas grave. Elle n’est clairement pas une séquence de caractères ou une liste. Cela signifie que la signature de méthode Object est utilisée.

Types Primitifs

Les primitifs fonctionnent d’une manière similaire aux variables de référence. Java essaie de trouver la méthode surchargée correspondante la plus spécifique. Que pensez-vous qu’il se passe ici ?

public class Autruche {
    public void voler(int i) {
        System.out.print("int");
    }
    public void voler(long l) {
        System.out.print("long");
    }
    public static void main(String[] args) {
        var p = new Autruche();
        p.voler(123);
        System.out.print("-");
        p.voler(123L);
    }
}

La réponse est int-long. Le premier appel passe un int et voit une correspondance exacte. Le second appel passe un long et voit également une correspondance exacte. Si nous commentons la méthode surchargée avec la liste de paramètres int, la sortie devient long-long. Java n’a aucun problème à appeler un primitif plus grand. Cependant, il ne le fera pas à moins qu’une meilleure correspondance ne soit pas trouvée.

Autoboxing

Comme nous l’avons vu précédemment, l’autoboxing s’applique aux appels de méthode, mais que se passe-t-il si vous avez à la fois une version primitive et une version Integer ?

public class Kiwi {
    public void voler(int nbKilometres) {}
    public void voler(Integer nbKilometres) {}
}

Ces surcharges de méthodes sont valides. Java essaie d’utiliser la liste de paramètres la plus spécifique qu’il peut trouver. C’est vrai pour l’autoboxing ainsi que pour les autres types correspondants dont nous parlons dans cette section.

Cela signifie qu’appeler voler(3) appellera la première méthode. Lorsque la version primitive int n’est pas présente, Java effectuera l’autoboxing. Cependant, lorsque la version primitive int est fournie, il n’y a aucune raison pour que Java fasse le travail supplémentaire d’autoboxing.

Tableaux

Contrairement à l’exemple précédent, ce code n’effectue pas d’autoboxing :

public static void marcher(int[] ints) {}
public static void marcher(Integer[] integers) {}

Les tableaux existent depuis le début de Java. Ils spécifient leurs types réels.

Varargs

Quelle méthode pensez-vous être appelée si nous passons un int[] ?

public class Toucan {
    public void voler(int[] longueurs) {}
    public void voler(int... longueurs) {} // NE COMPILE PAS
}

Question piège ! Rappelez-vous que Java traite les varargs comme s’ils étaient un tableau. Cela signifie que la signature de méthode est la même pour les deux méthodes. Puisque nous ne sommes pas autorisés à surcharger des méthodes avec la même liste de paramètres, ce code ne compile pas. Même si le code ne semble pas identique, il compile avec la même liste de paramètres.

Maintenant que nous venons d’expliquer que les deux méthodes sont similaires, il est temps de mentionner en quoi elles sont différentes. Il ne devrait pas être surprenant que vous puissiez appeler l’une ou l’autre méthode en passant un tableau :

voler(new int[] { 1, 2, 3 }); // Autorisé à appeler n'importe quelle méthode voler()

Cependant, vous ne pouvez appeler que la version varargs avec des paramètres autonomes :

voler(1, 2, 3); // Autorisé à appeler uniquement la méthode voler() utilisant varargs

Évidemment, cela signifie qu’ils ne compilent pas exactement de la même façon. La liste de paramètres est la même, cependant, et c’est ce que vous devez savoir par rapport à la surcharge.

Tout Rassembler

Jusqu’à présent, toutes les règles pour déterminer quand une méthode surchargée est appelée devraient être logiques. Java appelle la méthode la plus spécifique qu’il peut. Lorsque certains types interagissent, les règles de Java se concentrent sur la compatibilité ascendante. Il y a longtemps, l’autoboxing et les varargs n’existaient pas. Comme le code ancien doit toujours fonctionner, cela signifie que l’autoboxing et les varargs viennent en dernier lorsque Java examine les méthodes surchargées. Prêt pour l’ordre officiel ? Le Tableau 5.6 vous le présente.

RègleExemple de ce qui sera choisi pour planer(1,2)
Correspondance exacte par typeString planer(int i, int j)
Type primitif plus grandString planer(long i, long j)
Type autoboxéString planer(Integer i, Integer j)
VarargsString planer(int… nums)

Donnons un essai pratique en utilisant les règles du Tableau 5.6. Que pensez-vous que cela affiche ?

public class Planeur {
    public static String planer(String s) {
        return "1";
    }
    public static String planer(String... s) {
        return "2";
    }
    public static String planer(Object o) {
        return "3";
    }
    public static String planer(String s, String t) {
        return "4";
    }
    public static void main(String[] args) {
        System.out.print(planer("a"));
        System.out.print(planer("a", "b"));
        System.out.print(planer("a", "b", "c"));
    }
}

Cela affiche 142. Le premier appel correspond à la signature prenant une seule String car c’est la correspondance la plus spécifique. Le deuxième appel correspond à la signature prenant deux paramètres String puisque c’est une correspondance exacte. Ce n’est qu’au troisième appel que la version varargs est utilisée puisqu’il n’y a pas de meilleures correspondances.