Polymorphisme à Java

1. Vue d'ensemble

Tous les langages de programmation orientée objet (POO) doivent présenter quatre caractéristiques de base: l'abstraction, l'encapsulation, l'héritage et le polymorphisme.

Dans cet article, nous couvrons deux types principaux de polymorphisme: polymorphisme statique ou la compilation et dynamique ou exécution polymorphisme . Le polymorphisme statique est appliqué au moment de la compilation tandis que le polymorphisme dynamique est réalisé au moment de l'exécution.

2. Polymorphisme statique

Selon Wikipedia, le polymorphisme statique est une imitation du polymorphisme qui est résolu au moment de la compilation et supprime ainsi les recherches de tables virtuelles à l'exécution .

Par exemple, notre classe TextFile dans une application de gestion de fichiers peut avoir trois méthodes avec la même signature de la méthode read () :

public class TextFile extends GenericFile { //... public String read() { return this.getContent() .toString(); } public String read(int limit) { return this.getContent() .toString() .substring(0, limit); } public String read(int start, int stop) { return this.getContent() .toString() .substring(start, stop); } }

Lors de la compilation du code, le compilateur vérifie que toutes les invocations de la méthode read correspondent à au moins l'une des trois méthodes définies ci-dessus.

3. Polymorphisme dynamique

Avec le polymorphisme dynamique, la machine virtuelle Java (JVM) gère la détection de la méthode appropriée à exécuter lorsqu'une sous-classe est affectée à sa forme parent . Cela est nécessaire car la sous-classe peut remplacer certaines ou toutes les méthodes définies dans la classe parent.

Dans une application de gestionnaire de fichiers hypothétique, définissons la classe parente pour tous les fichiers appelés GenericFile :

public class GenericFile { private String name; //... public String getFileInfo() { return "Generic File Impl"; } }

Nous pouvons également implémenter une classe ImageFile qui étend le GenericFile mais remplace la méthode getFileInfo () et ajoute plus d'informations:

public class ImageFile extends GenericFile { private int height; private int width; //... getters and setters public String getFileInfo() { return "Image File Impl"; } }

Lorsque nous créons une instance de ImageFile et l' affectons à une classe GenericFile , un cast implicite est effectué. Cependant, la JVM garde une référence à la forme réelle de ImageFile .

La construction ci-dessus est analogue à la substitution de méthode. Nous pouvons le confirmer en invoquant la méthode getFileInfo () en:

public static void main(String[] args) { GenericFile genericFile = new ImageFile("SampleImageFile", 200, 100, new BufferedImage(100, 200, BufferedImage.TYPE_INT_RGB) .toString() .getBytes(), "v1.0.0"); logger.info("File Info: \n" + genericFile.getFileInfo()); }

Comme prévu, genericFile.getFileInfo () déclenche la méthode getFileInfo () de la classe ImageFile comme indiqué dans la sortie ci-dessous:

File Info: Image File Impl

4. Autres caractéristiques polymorphes à Java

En plus de ces deux principaux types de polymorphisme en Java, il existe d'autres caractéristiques du langage de programmation Java qui présentent un polymorphisme. Discutons de certaines de ces caractéristiques.

4.1. Coercition

La coercition polymorphe traite de la conversion de type implicite effectuée par le compilateur pour éviter les erreurs de type. Un exemple typique est vu dans une concaténation d'entiers et de chaînes:

String str = “string” + 2;

4.2. Surcharge de l'opérateur

La surcharge d'opérateur ou de méthode fait référence à une caractéristique polymorphe du même symbole ou opérateur ayant des significations (formes) différentes selon le contexte.

Par exemple, le symbole plus (+) peut être utilisé pour l'addition mathématique ainsi que la concaténation de chaînes . Dans les deux cas, seul le contexte (c'est-à-dire les types d'arguments) détermine l'interprétation du symbole:

String str = "2" + 2; int sum = 2 + 2; System.out.printf(" str = %s\n sum = %d\n", str, sum);

Production:

str = 22 sum = 4

4.3. Paramètres polymorphes

Le polymorphisme paramétrique permet d'associer le nom d'un paramètre ou d'une méthode dans une classe à différents types. Nous avons un exemple typique ci-dessous où nous définissons le contenu comme une chaîne et plus tard comme un entier :

public class TextFile extends GenericFile { private String content; public String setContentDelimiter() { int content = 100; this.content = this.content + content; } }

Il est également important de noter que la déclaration de paramètres polymorphes peut conduire à un problème connu sous le nom de masquage de variable où une déclaration locale d'un paramètre remplace toujours la déclaration globale d'un autre paramètre du même nom.

Pour résoudre ce problème, il est souvent conseillé d'utiliser des références globales telles que ce mot clé pour pointer vers des variables globales dans un contexte local.

4.4. Sous-types polymorphes

Le sous-type polymorphe nous permet, de manière pratique, d'assigner plusieurs sous-types à un type et d'attendre que toutes les invocations sur le type déclenchent les définitions disponibles dans le sous-type.

Par exemple, si nous avons une collection de GenericFile et que nous invoquons la méthode getInfo () sur chacun d'eux, nous pouvons nous attendre à ce que la sortie soit différente en fonction du sous-type dont chaque élément de la collection est dérivé:

GenericFile [] files = {new ImageFile("SampleImageFile", 200, 100, new BufferedImage(100, 200, BufferedImage.TYPE_INT_RGB).toString() .getBytes(), "v1.0.0"), new TextFile("SampleTextFile", "This is a sample text content", "v1.0.0")}; for (int i = 0; i < files.length; i++) { files[i].getInfo(); }

Le polymorphisme de sous-type est rendu possible par une combinaison de remontée et de liaison tardive . L'upcasting implique le transtypage de la hiérarchie d'héritage d'un supertype vers un sous-type:

ImageFile imageFile = new ImageFile(); GenericFile file = imageFile;

The resulting effect of the above is that ImageFile-specific methods cannot be invoked on the new upcast GenericFile. However, methods in the subtype override similar methods defined in the supertype.

To resolve the problem of not being able to invoke subtype-specific methods when upcasting to a supertype, we can do a downcasting of the inheritance from a supertype to a subtype. This is done by:

ImageFile imageFile = (ImageFile) file;

Late bindingstrategy helps the compiler to resolve whose method to trigger after upcasting. In the case of imageFile#getInfo vs file#getInfo in the above example, the compiler keeps a reference to ImageFile‘s getInfo method.

5. Problems With Polymorphism

Let's look at some ambiguities in polymorphism that could potentially lead to runtime errors if not properly checked.

5.1. Type Identification During Downcasting

Recall that we earlier lost access to some subtype-specific methods after performing an upcast. Although we were able to solve this with a downcast, this does not guarantee actual type checking.

For example, if we perform an upcast and subsequent downcast:

GenericFile file = new GenericFile(); ImageFile imageFile = (ImageFile) file; System.out.println(imageFile.getHeight());

We notice that the compiler allows a downcast of a GenericFile into an ImageFile, even though the class actually is a GenericFile and not an ImageFile.

Consequently, if we try to invoke the getHeight() method on the imageFile class, we get a ClassCastException as GenericFile does not define getHeight() method:

Exception in thread "main" java.lang.ClassCastException: GenericFile cannot be cast to ImageFile

To solve this problem, the JVM performs a Run-Time Type Information (RTTI) check. We can also attempt an explicit type identification by using the instanceof keyword just like this:

ImageFile imageFile; if (file instanceof ImageFile) { imageFile = file; }

The above helps to avoid a ClassCastException exception at runtime. Another option that may be used is wrapping the cast within a try and catch block and catching the ClassCastException.

It should be noted that RTTI check is expensive due to the time and resources needed to effectively verify that a type is correct. In addition, frequent use of the instanceof keyword almost always implies a bad design.

5.2. Fragile Base Class Problem

According to Wikipedia, base or superclasses are considered fragile if seemingly safe modifications to a base class may cause derived classes to malfunction.

Let's consider a declaration of a superclass called GenericFile and its subclass TextFile:

public class GenericFile { private String content; void writeContent(String content) { this.content = content; } void toString(String str) { str.toString(); } }
public class TextFile extends GenericFile { @Override void writeContent(String content) { toString(content); } }

When we modify the GenericFile class:

public class GenericFile { //... void toString(String str) { writeContent(str); } }

Nous observons que la modification ci-dessus laisse TextFile dans une récursion infinie dans la méthode writeContent () , ce qui aboutit finalement à un débordement de pile.

Pour résoudre un problème de classe de base fragile, nous pouvons utiliser le mot-clé final pour empêcher les sous-classes de remplacer la méthode writeContent () . Une bonne documentation peut également aider. Et last but not least, la composition doit généralement être préférée à l'héritage.

6. Conclusion

Dans cet article, nous avons discuté du concept fondamental du polymorphisme, en nous concentrant à la fois sur les avantages et les inconvénients.

Comme toujours, le code source de cet article est disponible à l'adresse over sur GitHub.