Polymorphisme Java et ses types

Le polymorphisme fait référence à la capacité de certaines entités à se présenter sous différentes formes. Il est populairement représenté par le papillon, qui se transforme de larve en pupe en imago. Le polymorphisme existe également dans les langages de programmation, en tant que technique de modélisation qui vous permet de créer une interface unique vers divers opérandes, arguments et objets. Le polymorphisme Java aboutit à un code plus concis et plus facile à maintenir.

Bien que ce didacticiel se concentre sur le polymorphisme des sous-types, il existe plusieurs autres types que vous devez connaître. Nous commencerons par un aperçu des quatre types de polymorphisme.

télécharger Obtenir le code Téléchargez le code source des exemples d'applications dans ce didacticiel. Créé par Jeff Friesen pour JavaWorld.

Types de polymorphisme à Java

Il existe quatre types de polymorphisme en Java:

  1. La coercition est une opération qui sert plusieurs types via une conversion de type implicite. Par exemple, vous divisez un entier par un autre entier ou une valeur à virgule flottante par une autre valeur à virgule flottante. Si un opérande est un entier et l'autre opérande est une valeur à virgule flottante, le compilateur contraint (convertit implicitement) l'entier en une valeur à virgule flottante pour éviter une erreur de type. (Il n'existe aucune opération de division qui prend en charge un opérande entier et un opérande à virgule flottante.) Un autre exemple consiste à transmettre une référence d'objet de sous-classe au paramètre de superclasse d'une méthode. Le compilateur contraint le type de sous-classe au type de superclasse pour limiter les opérations à celles de la superclasse.
  2. La surcharge fait référence à l'utilisation du même symbole d'opérateur ou du même nom de méthode dans différents contextes. Par exemple, vous pouvez utiliser +pour effectuer une addition d'entiers, une addition à virgule flottante ou une concaténation de chaînes, selon les types de ses opérandes. De plus, plusieurs méthodes ayant le même nom peuvent apparaître dans une classe (par déclaration et / ou héritage).
  3. Le polymorphisme paramétrique stipule que dans une déclaration de classe, un nom de champ peut s'associer à différents types et un nom de méthode peut s'associer à différents paramètres et types de retour. Le champ et la méthode peuvent alors prendre différents types dans chaque instance de classe (objet). Par exemple, un champ peut être de type Double(un membre de la bibliothèque de classes standard de Java qui encapsule une doublevaleur) et une méthode peut renvoyer a Doubledans un objet, et le même champ peut être de type Stringet la même méthode peut renvoyer a Stringdans un autre objet . Java prend en charge le polymorphisme paramétrique via des génériques, dont je parlerai dans un prochain article.
  4. Sous-type signifie qu'un type peut servir de sous-type d'un autre type. Lorsqu'une instance de sous-type apparaît dans un contexte de supertype, l'exécution d'une opération de supertype sur l'instance de sous-type entraîne l'exécution de la version du sous-type de cette opération. Par exemple, considérons un fragment de code qui dessine des formes arbitraires. Vous pouvez exprimer ce code de dessin de manière plus concise en introduisant une Shapeclasse avec une draw()méthode; en introduisant Circle, Rectangleet d'autres sous-classes qui remplacent draw(); en introduisant un tableau de type Shapedont les éléments stockent des références à des Shapeinstances de sous-classes; et en appelant Shapela draw()méthode de sur chaque instance. Lorsque vous appelez draw(), ce sont les Circle's, Rectangle' s ou une autre Shapeinstancedraw()méthode qui est appelée. Nous disons qu'il ya beaucoup de formes de Shapede » draw()la méthode.

Ce didacticiel présente le polymorphisme des sous-types. Vous en apprendrez davantage sur la conversion ascendante et la liaison tardive, les classes abstraites (qui ne peuvent pas être instanciées) et les méthodes abstraites (qui ne peuvent pas être appelées). Vous en apprendrez également sur le downcasting et l'identification du type d'exécution, et vous aurez un premier aperçu des types de retour covariants. Je vais enregistrer le polymorphisme paramétrique pour un futur tutoriel.

Polymorphisme ad hoc vs universel

Comme beaucoup de développeurs, je classe la coercition et la surcharge comme polymorphisme ad hoc, et paramétrique et sous-type comme polymorphisme universel. Bien que des techniques précieuses, je ne crois pas que la coercition et la surcharge soient un véritable polymorphisme; ils ressemblent plus à des conversions de type et à du sucre syntaxique.

Polymorphisme de sous-type: Upcasting et liaison tardive

Le polymorphisme des sous-types repose sur une remontée et une liaison tardive. L'upcasting est une forme de transtypage où vous transformez la hiérarchie d'héritage d'un sous-type en un supertype. Aucun opérateur de cast n'est impliqué car le sous-type est une spécialisation du supertype. Par exemple, les Shape s = new Circle();upcasts de Circleà Shape. Cela a du sens car un cercle est une sorte de forme.

Après la conversion ascendante Circlevers Shape, vous ne pouvez pas appeler de Circleméthodes spécifiques, comme une getRadius()méthode qui renvoie le rayon du cercle, car les Circleméthodes spécifiques ne font pas partie de Shapel'interface de. Perdre l'accès aux caractéristiques de sous-type après avoir réduit une sous-classe à sa superclasse semble inutile, mais est nécessaire pour obtenir un polymorphisme de sous-type.

Supposons que Shapedéclare une draw()méthode, sa Circlesous-classe remplace cette méthode, Shape s = new Circle();vient de s'exécuter et la ligne suivante spécifie s.draw();. Quelle draw()méthode est appelée: méthode Shapes draw()ou méthode Circles draw()? Le compilateur ne sait pas quelle draw()méthode appeler. Tout ce qu'il peut faire est de vérifier qu'une méthode existe dans la superclasse et de vérifier que la liste des arguments de l'appel de méthode et le type de retour correspondent à la déclaration de méthode de la superclasse. Cependant, le compilateur insère également une instruction dans le code compilé qui, au moment de l'exécution, récupère et utilise toute référence spour appeler la draw()méthode correcte . Cette tâche est connue sous le nom de liaison tardive .

Reliure tardive vs liaison anticipée

La liaison tardive est utilisée pour les appels à des finalméthodes non- instance. Pour tous les autres appels de méthode, le compilateur sait quelle méthode appeler. Il insère une instruction dans le code compilé qui appelle la méthode associée au type de la variable et non à sa valeur. Cette technique est connue sous le nom de liaison précoce .

J'ai créé une application qui démontre le polymorphisme des sous-types en termes de remontée et de liaison tardive. Cette application se compose de Shape, Circle, Rectangleet les Shapesclasses, où chaque classe est stocké dans son propre fichier source. Le listing 1 présente les trois premières classes.

Listing 1. Déclarer une hiérarchie de formes

class Shape { void draw() { } } class Circle extends Shape { private int x, y, r; Circle(int x, int y, int r) { this.x = x; this.y = y; this.r = r; } // For brevity, I've omitted getX(), getY(), and getRadius() methods. @Override void draw() { System.out.println("Drawing circle (" + x + ", "+ y + ", " + r + ")"); } } class Rectangle extends Shape { private int x, y, w, h; Rectangle(int x, int y, int w, int h) { this.x = x; this.y = y; this.w = w; this.h = h; } // For brevity, I've omitted getX(), getY(), getWidth(), and getHeight() // methods. @Override void draw() { System.out.println("Drawing rectangle (" + x + ", "+ y + ", " + w + "," + h + ")"); } }

Le listing 2 présente la Shapesclasse d'application dont la main()méthode pilote l'application.

Listing 2. Upcasting and late binding in subtype polymorphism

class Shapes { public static void main(String[] args) { Shape[] shapes = { new Circle(10, 20, 30), new Rectangle(20, 30, 40, 50) }; for (int i = 0; i < shapes.length; i++) shapes[i].draw(); } }

The declaration of the shapes array demonstrates upcasting. The Circle and Rectangle references are stored in shapes[0] and shapes[1] and are upcast to type Shape. Each of shapes[0] and shapes[1] is regarded as a Shape instance: shapes[0] isn't regarded as a Circle; shapes[1] isn't regarded as a Rectangle.

Late binding is demonstrated by the shapes[i].draw(); expression. When i equals 0, the compiler-generated instruction causes Circle's draw() method to be called. When i equals 1, however, this instruction causes Rectangle's draw() method to be called. This is the essence of subtype polymorphism.

Assuming that all four source files (Shapes.java, Shape.java, Rectangle.java, and Circle.java) are located in the current directory, compile them via either of the following command lines:

javac *.java javac Shapes.java

Run the resulting application:

java Shapes

You should observe the following output:

Drawing circle (10, 20, 30) Drawing rectangle (20, 30, 40, 50)

Abstract classes and methods

When designing class hierarchies, you'll find that classes nearer the top of these hierarchies are more generic than classes that are lower down. For example, a Vehicle superclass is more generic than a Truck subclass. Similarly, a Shape superclass is more generic than a Circle or a Rectangle subclass.

It doesn't make sense to instantiate a generic class. After all, what would a Vehicle object describe? Similarly, what kind of shape is represented by a Shape object? Rather than code an empty draw() method in Shape, we can prevent this method from being called and this class from being instantiated by declaring both entities to be abstract.

Java provides the abstract reserved word to declare a class that cannot be instantiated. The compiler reports an error when you try to instantiate this class. abstract is also used to declare a method without a body. The draw() method doesn't need a body because it is unable to draw an abstract shape. Listing 3 demonstrates.

Listing 3. Abstracting the Shape class and its draw() method

abstract class Shape { abstract void draw(); // semicolon is required }

Abstract cautions

The compiler reports an error when you attempt to declare a class abstract and final. For example, the compiler complains about abstract final class Shape because an abstract class cannot be instantiated and a final class cannot be extended. The compiler also reports an error when you declare a method abstract but don't declare its class abstract. Removing abstract from the Shape class's header in Listing 3 would result in an error, for instance. This would be an error because a non-abstract (concrete) class cannot be instantiated when it contains an abstract method. Finally, when you extend an abstract class, the extending class must override all of the abstract methods, or else the extending class must itself be declared to be abstract; otherwise, the compiler will report an error.

An abstract class can declare fields, constructors, and non-abstract methods in addition to or instead of abstract methods. For example, an abstract Vehicle class might declare fields describing its make, model, and year. Also, it might declare a constructor to initialize these fields and concrete methods to return their values. Check out Listing 4.

Listing 4. Abstracting a vehicle

abstract class Vehicle { private String make, model; private int year; Vehicle(String make, String model, int year) { this.make = make; this.model = model; this.year = year; } String getMake() { return make; } String getModel() { return model; } int getYear() { return year; } abstract void move(); }

You'll note that Vehicle declares an abstract move() method to describe the movement of a vehicle. For example, a car rolls down the road, a boat sails across the water, and a plane flies through the air. Vehicle's subclasses would override move() and provide an appropriate description. They would also inherit the methods and their constructors would call Vehicle's constructor.

Downcasting and RTTI

Moving up the class hierarchy, via upcasting, entails losing access to subtype features. For example, assigning a Circle object to Shape variable s means that you cannot use s to call Circle's getRadius() method. However, it's possible to once again access Circle's getRadius() method by performing an explicit cast operation like this one: Circle c = (Circle) s;.

This assignment is known as downcasting because you are casting down the inheritance hierarchy from a supertype to a subtype (from the Shape superclass to the Circle subclass). Although an upcast is always safe (the superclass's interface is a subset of the subclass's interface), a downcast isn't always safe. Listing 5 shows what kind of trouble could ensue if you use downcasting incorrectly.

Listing 5. The problem with downcasting

class Superclass { } class Subclass extends Superclass { void method() { } } public class BadDowncast { public static void main(String[] args) { Superclass superclass = new Superclass(); Subclass subclass = (Subclass) superclass; subclass.method(); } }

Listing 5 presents a class hierarchy consisting of Superclass and Subclass, which extends Superclass. Furthermore, Subclass declares method(). A third class named BadDowncast provides a main() method that instantiates Superclass. BadDowncast then tries to downcast this object to Subclass and assign the result to variable subclass.

Dans ce cas, le compilateur ne se plaindra pas car le downcasting d'une superclasse vers une sous-classe dans la même hiérarchie de types est légal. Cela dit, si l'affectation était autorisée, l'application se planterait lorsqu'elle tentait de s'exécuter subclass.method();. Dans ce cas, la machine virtuelle Java tenterait d'appeler une méthode inexistante, car Superclassne déclare pas method(). Heureusement, la machine virtuelle Java vérifie qu'une conversion est légale avant d'effectuer une opération de conversion. Détecter que Superclasscela ne déclare pas method(), cela jetterait un ClassCastExceptionobjet. (Je discuterai des exceptions dans un prochain article.)

Compilez le listing 5 comme suit:

javac BadDowncast.java

Exécutez l'application résultante:

java BadDowncast