Surcharge et écrasement de méthode en Java

1. Vue d'ensemble

La surcharge et le remplacement de méthode sont des concepts clés du langage de programmation Java, et en tant que tels, ils méritent un examen approfondi.

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

2. Surcharge de méthode

La surcharge de méthode est un mécanisme puissant qui nous permet de définir des API de classe cohésives. Pour mieux comprendre pourquoi la surcharge de méthodes est une fonctionnalité si 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. Voici où la surcharge de méthode 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 des nombres d'arguments différents
  • implémenter deux méthodes ou plus qui ont le même nom mais prennent des arguments de types différents

2.1. Différents nombres d'arguments

La classe Multiplier montre, en un mot, comment surcharger la méthode multiply () en définissant simplement deux implémentations qui prennent des nombres d'arguments différents:

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éthode:

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 de noter, cependant, qu'il n'est pas possible d'avoir deux implémentations de méthode qui ne diffèrent que par leurs types 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 compilerait tout simplement pas à cause de l'ambiguïté de l'appel de méthode - le compilateur ne saurait pas quelle implémentation de multiply () appeler.

2.3. Promotion de type

Une fonctionnalité intéressante fournie par la surcharge de méthode est la soi-disant promotion de type, alias élargissement de la conversion primitive .

En termes simples, un type donné est implicitement promu vers un autre lorsqu'il n'y a pas de correspondance entre les types des arguments passés à la méthode surchargée et une implémentation de méthode spécifique.

Pour comprendre plus clairement le fonctionnement de la promotion de type, considérez 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; } 

Maintenant, l'appel de la méthode avec deux arguments int entraînera la promotion du deuxième argument en long , car dans ce cas, il n'y a 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, la promotion de type n'a tout simplement 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éthode:

  • 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 long, float ou double
  • long peut être promu flottant ou doublé
  • float peut être promu au double

2.4. Reliure statique

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

Dans le cas de la surcharge de méthode, la liaison est effectuée de manière statique au moment de la compilation, d'où le nom de liaison statique.

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

3. Remplacement de méthode

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

Bien que le remplacement de méthode soit une fonctionnalité puissante - étant donné qu'il s'agit d'une conséquence logique de l'utilisation de l'héritage, l'un des plus grands piliers de la POO - quand et où l'utiliser doit être analysé avec soin, au cas par cas .

Voyons maintenant comment utiliser la substitution de méthode en créant une relation simple, basée sur l'héritage («is-a»).

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 raffinée pour le sous-type Car.

Ici, il est clair de constater que si une utilisations d'application instances du véhicule classe, alors il peut travailler avec les instances de voiture et , comme les deux mises en œuvre de l' accélération () méthode 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, renvoient 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 Car renverra 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. Substituabilité de type

Un principe fondamental de la POO est celui de la substituabilité de type, qui est étroitement associé au principe de substitution de Liskov (LSP).

En termes simples, 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é des types est correctement préservée.

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

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

  • Si une méthode de la classe de base prend des arguments d'un type donné, la méthode surchargée doit prendre le même type ou un supertype (aka arguments de méthode contravariants )
  • Si une méthode de la classe de base retourne void , la méthode surchargée doit retourner void
  • Si une méthode de la classe de base renvoie une primitive, la méthode surchargée doit renvoyer la même primitive
  • Si une méthode de la classe de base renvoie un certain type, la méthode surchargée doit renvoyer le même type ou un sous-type (aka type de retour covariant )
  • Si une méthode de la classe de base lève une exception, la méthode remplacée doit lever la même exception ou un sous-type de l'exception de classe de base

3.2. Liaison dynamique

Étant donné que le remplacement de méthode ne peut être implémenté qu'avec l'héritage, où il existe une hiérarchie d'un type de base et d'un ou plusieurs sous-types, le compilateur ne peut pas déterminer au moment de la compilation quelle méthode appeler, car la classe de base et les sous-classes définissent le 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 a lieu au moment de l'exécution, la substitution de méthode est un exemple typique de liaison dynamique.

4. Conclusion

Dans ce didacticiel, nous avons appris à implémenter la surcharge de méthode et le remplacement de méthode, et nous avons exploré certaines situations typiques où elles sont utiles.

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