Méthode surchargée et surchargée en Java

1. Vue d’ensemble

La surcharge et le dépassement de méthodes sont des concepts clés du langage de programmation Java et, en tant que tels, méritent une analyse approfondie.

Dans cet article, nous allons apprendre les bases de ces concepts et voir dans quelles situations ils peuvent être utiles.

2. Méthode de surcharge

  • La surcharge de méthodes est un mécanisme puissant qui nous permet de définir des API de classe cohérentes. ** Pour mieux comprendre pourquoi la surcharge de méthodes est une fonctionnalité aussi précieuse, voyons un exemple simple.

Supposons que nous ayons écrit une classe d’utilité naïve qui implémente différentes méthodes pour multiplier deux nombres, trois nombres, etc.

Si nous avons donné aux méthodes des noms trompeurs ou ambigus, tels que multiply2 () , multiply3 () , multiply4 (), , alors ce serait une API de classe mal conçue. C’est là que la surcharge de méthodes entre en jeu.

  • En termes simples, nous pouvons implémenter la surcharge de méthode de deux manières différentes: **

  • implémenter deux ** méthodes ou plus qui ont le même nom mais prennent

différents nombres d’arguments implémenter deux ** méthodes ou plus qui ont le même nom mais prennent

arguments de différents types **

2.1. Différents nombres d’arguments

La classe Multiplier montre, en quelques mots, comment surcharger la méthode multiply () en définissant simplement deux implémentations qui prennent un nombre différent d’arguments:

public class Multiplier {

    public int multiply(int a, int b) {
        return a **  b;
    }

    public int multiply(int a, int b, int c) {
        return a **  b **  c;
    }
}

2.2. Arguments de différents types

De même, nous pouvons surcharger la méthode multiply () en lui faisant accepter des arguments de types différents:

public class Multiplier {

    public int multiply(int a, int b) {
        return a **  b;
    }

    public double multiply(double a, double b) {
        return a **  b;
    }
}

De plus, il est légitime de définir la classe Multiplier avec les deux types de surcharge de méthodes:

public class Multiplier {

    public int multiply(int a, int b) {
        return a **  b;
    }

    public int multiply(int a, int b, int c) {
        return a **  b **  c;
    }

    public double multiply(double a, double b) {
        return a **  b;
    }
}

Il convient toutefois de noter que il n’est pas possible d’avoir deux implémentations de méthodes qui diffèrent uniquement par leur type de retour .

Pour comprendre pourquoi, considérons l’exemple suivant:

public int multiply(int a, int b) {
    return a **  b;
}

public double multiply(int a, int b) {
    return a **  b;
}

Dans ce cas, le code ne serait simplement pas compilé à cause de l’ambiguïté de la méthode - le compilateur ne saurait pas quelle implémentation de multiply () appeler.

2.3. Type Promotion

Une fonctionnalité intéressante fournie par la surcharge de méthodes est la soi-disant promotion type, a.k.a. .

En termes simples, un type donné est implicitement promu en un autre quand il n’ya aucune correspondance entre les types des arguments passés à la méthode surchargée et une implémentation de méthode spécifique.

Pour mieux comprendre le fonctionnement du type promotion, considérons les implémentations suivantes de la méthode multiply () :

public double multiply(int a, long b) {
    return a **  b;
}

public int multiply(int a, int b, int c) {
    return a **  b **  c;
}

Désormais, si vous appelez la méthode avec deux arguments int , le deuxième argument sera promu à long , car dans ce cas, il n’ya pas d’implémentation correspondante de la méthode avec deux arguments int .

Voyons un test unitaire rapide pour démontrer la promotion de type:

@Test
public void whenCalledMultiplyAndNoMatching__thenTypePromotion() {
    assertThat(multiplier.multiply(10, 10)).isEqualTo(100.0);
}

Inversement, si nous appelons la méthode avec une implémentation correspondante, le type promotion n’a pas lieu:

@Test
public void whenCalledMultiplyAndMatching__thenNoTypePromotion() {
    assertThat(multiplier.multiply(10, 10, 10)).isEqualTo(1000);
}

Voici un résumé des règles de promotion de type qui s’appliquent à la surcharge de méthodes:

  • byte peut être promu en short, int, long, float, ou double

  • short peut être promu en int, long, float, ou double

  • char peut être promu en int, long, float, ou double

  • int peut être promu en long, float, ou double

  • long peut être promu à float ou double

  • float peut être promu à double

2.4. Liaison statique

La possibilité d’associer un appel de méthode spécifique au corps de la méthode est appelée liaison.

En cas de surcharge de la méthode, la liaison est effectuée statiquement lors de la compilation, elle est donc appelée liaison statique.

Le compilateur peut effectivement définir la liaison au moment de la compilation en vérifiant simplement les signatures des méthodes.

3. Méthode prioritaire

  • Le remplacement de méthode nous permet de fournir des implémentations détaillées dans des sous-classes pour des méthodes définies dans une classe de base.

Bien que le remplacement de méthode soit une fonctionnalité puissante - sachant que c’est une conséquence logique de l’utilisation de https://en.wikipedia.org/wiki/Inheritance (object-oriented programming)[inheritance], l’un des principaux piliers de https://en . wikipedia.org/wiki/Object-oriented__programming[OOP]- Le moment et le lieu d’utilisation devraient être analysés avec soin, cas par cas .

Voyons maintenant comment utiliser la méthode redéfinie en créant une relation simple (basée sur l’héritage)

Voici la classe de base:

public class Vehicle {

    public String accelerate(long mph) {
        return "The vehicle accelerates at : " + mph + " MPH.";
    }

    public String stop() {
        return "The vehicle has stopped.";
    }

    public String run() {
        return "The vehicle is running.";
    }
}

Et voici une sous-classe artificielle:

public class Car extends Vehicle {

    @Override
    public String accelerate(long mph) {
        return "The car accelerates at : " + mph + " MPH.";
    }
}

Dans la hiérarchie ci-dessus, nous avons simplement remplacé la méthode accelerate () afin de fournir une implémentation plus fine du sous-type Car.

Ici, il est clair que si une application utilise des instances de la classe Vehicle , elle peut également fonctionner avec des instances de Car , car les deux implémentations de la méthode accelerate () ont la même signature et le même type de retour.

Écrivons quelques tests unitaires pour vérifier les classes Vehicle et Car :

@Test
public void whenCalledAccelerate__thenOneAssertion() {
    assertThat(vehicle.accelerate(100))
      .isEqualTo("The vehicle accelerates at : 100 MPH.");
}

@Test
public void whenCalledRun__thenOneAssertion() {
    assertThat(vehicle.run())
      .isEqualTo("The vehicle is running.");
}

@Test
public void whenCalledStop__thenOneAssertion() {
    assertThat(vehicle.stop())
      .isEqualTo("The vehicle has stopped.");
}

@Test
public void whenCalledAccelerate__thenOneAssertion() {
    assertThat(car.accelerate(80))
      .isEqualTo("The car accelerates at : 80 MPH.");
}

@Test
public void whenCalledRun__thenOneAssertion() {
    assertThat(car.run())
      .isEqualTo("The vehicle is running.");
}

@Test
public void whenCalledStop__thenOneAssertion() {
    assertThat(car.stop())
      .isEqualTo("The vehicle has stopped.");
}

Voyons maintenant quelques tests unitaires qui montrent comment les méthodes run () et stop () , qui ne sont pas remplacées, retournent des valeurs égales pour Car et Vehicle :

@Test
public void givenVehicleCarInstances__whenCalledRun__thenEqual() {
    assertThat(vehicle.run()).isEqualTo(car.run());
}

@Test
public void givenVehicleCarInstances__whenCalledStop__thenEqual() {
   assertThat(vehicle.stop()).isEqualTo(car.stop());
}

Dans notre cas, nous avons accès au code source pour les deux classes, nous pouvons donc clairement voir que l’appel de la méthode accelerate () sur une instance de base Vehicle et l’appel de accelerate () sur une instance de Car renverront des valeurs différentes pour le même argument.

Par conséquent, le test suivant montre que la méthode remplacée est appelée pour une instance de Car :

@Test
public void whenCalledAccelerateWithSameArgument__thenNotEqual() {
    assertThat(vehicle.accelerate(100))
      .isNotEqualTo(car.accelerate(100));
}

** 3.1. Type de substituabilité

**

Un principe fondamental de la programmation orientée objet est celui de la substituabilité de type, qui est étroitement associé au https://en.wikipedia.org/wiki/Liskov substitution principle[Liskov Substitution Principle (LSP)].

En résumé, le LSP indique que si une application fonctionne avec un type de base donné, elle doit également fonctionner avec l’un de ses sous-types . De cette façon, la substituabilité de type est correctement préservée.

  • Le plus gros problème avec le remplacement de méthode est que certaines implémentations de méthodes spécifiques dans les classes dérivées pourraient ne pas adhérer pleinement au LSP et ne pourraient donc pas préserver la substituabilité de type. **

Bien sûr, il est valide de créer une méthode surchargée pour accepter des arguments de types différents et renvoyer également un type différent, mais en respectant scrupuleusement ces règles:

  • Si une méthode de la classe de base prend des arguments d’un type donné, le

La méthode surchargée doit prendre le même type ou un supertype (a.k.a.

arguments de la méthode contravariant ) ** Si une méthode de la classe de base retourne void , la méthode remplacée

devrait retourner void ** Si une méthode de la classe de base retourne une primitive, la substitution

méthode devrait retourner la même primitive ** Si une méthode de la classe de base retourne un certain type, le remplacement

La méthode doit renvoyer le même type ou un sous-type (a.k.a. covariant type de retour) ** Si une méthode de la classe de base lève une exception, la substitution

méthode doit générer la même exception ou un sous-type de l’exception de classe de base

3.2. Reliure dynamique

Considérant que le remplacement de méthode ne peut être implémenté qu’avec l’héritage, lorsqu’il existe une hiérarchie d’un type de base et d’un ou plusieurs sous-types, le compilateur ne peut pas déterminer lors de la compilation quelle méthode appeler, car mêmes méthodes.

En conséquence, le compilateur doit vérifier le type d’objet pour savoir quelle méthode doit être appelée.

Comme cette vérification est effectuée à l’exécution, le remplacement de méthode est un exemple typique de liaison dynamique.

4. Conclusion

Dans ce didacticiel, nous avons appris à mettre en œuvre la surcharge et le remplacement de méthodes, et nous avons exploré certaines situations typiques dans lesquelles elles sont utiles.

Comme d’habitude, tous les exemples de code présentés dans cet article sont disponibles à l’adresse https://github.com/eugenp/tutorials/tree/master/core-java-lang-oop sur GitHub].