« Amigo, tu aimes les baleines ? »

« Les baleines ? Je ne sais même pas ce que c'est. »

« C'est comme une vache, mais en plus grand, et ça nage. Soit dit en passant, les baleines descendent des vaches. Euh, du moins elles partagent un ancêtre commun. Peu importe. »

Polymorphisme et remplacement - 1

« Écoute bien. Je voudrais te parler d'un autre outil très puissant de la POO : le polymorphisme. Il a quatre caractéristiques. »

1) Remplacement de méthode.

Imagine que tu as écrit une classe « Cow » pour un jeu. Elle a beaucoup de variables et méthodes membres. Les objets de cette classe peuvent faire diverses choses : marcher, manger, dormir. Les vaches ont aussi une cloche qui sonne quand elles marchent. Disons que tu as tout implémenté dans cette classe dans les moindres détails.

Polymorphisme et remplacement - 2

Et là, d'un coup, le client dit qu'il veut sortir un nouveau niveau dans le jeu, où toutes les actions se déroulent dans la mer, et où le personnage principal est une baleine.

Tu as commencé à concevoir la classe Whale et tu te rends compte qu'elle ne diffère que légèrement de la classe Cow. Les deux classes utilisent une logique très similaire, et tu décides d'utiliser l'héritage.

La classe Cow est idéale pour être la classe parente : elle a déjà toutes les variables et méthodes nécessaires. Tout ce que tu as à faire est d'ajouter la capacité de la baleine à nager. Mais il y a un problème : ta baleine a des pattes, des cornes et une cloche. Après tout, la classe Cow implémente ces particularités. Que peux-tu faire ?

Polymorphisme et remplacement - 3

Le remplacement de méthode vient à la rescousse. Si nous héritons d'une méthode qui ne fait pas exactement ce dont nous avons besoin dans notre nouvelle classe, nous pouvons remplacer la méthode par une autre.

Polymorphisme et remplacement - 4

Comment fait-on cela ? Dans notre classe descendante, nous déclarons la méthode que nous voulons changer (avec la même signature que dans la classe parente). Ensuite, nous écrivons le nouveau code pour la méthode. Et c'est tout. C'est comme si l'ancienne méthode de la classe parente n'existait pas.

Voici comment cela fonctionne :

Code Description
class Cow
{
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a cow");
}
}class Whale extends Cow
{
public void printName()
{
System.out.println("I'm a whale");
}
}
Ici, nous définissons deux classes :  Cow et  WhaleWhale hérite de Cow.

La classe Whale remplace la méthode printName();.

public static void main(String[] args)
{
Cow cow = new Cow();
cow.printName();
}
Ce code affiche « Je suis une vache » à l'écran.
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printName();
}
Ce code affiche « Je suis une baleine » à l'écran

Après avoir hérité de Cow et remplacé printName, la classe Whale comporte réellement les données et méthodes suivantes :

Code Description
class Whale
{
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a whale");
}
}
Nous ne savons rien au sujet d'une ancienne méthode.

« Honnêtement, c'est ce à quoi je m'attendais. »

2) Mais ce n'est pas tout.

« Supposons que la classe Cow dispose d'une méthode printAll qui appelle les deux autres méthodes. Le code fonctionnerait comme ceci : »

L'écran affiche :
Je suis blanche
Je suis une baleine

Code Description
class Cow
{
public void printAll()
{
printColor();
printName();
}
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a cow");
}
}

class Whale extends Cow
{
public void printName()
{
System.out.println("I'm a whale");
}
}
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printAll();
}
L'écran affiche :
Je suis blanche
Je suis une baleine

Note que lorsque la méthode printAll() de la classe Cow est appelée, la méthode printName() de la classe Whale est utilisée, et non celle de Cow.

L'important n'est pas la classe dans laquelle la méthode est écrite, mais plutôt le type (la classe) de l'objet sur lequel la méthode est appelée.

« Je vois. »

« Tu peux seulement hériter de et remplacer les méthodes non static. Les méthodes static ne sont pas héritées et ne peuvent donc pas être remplacées. »

Voici à quoi ressemble la classe Whale une fois que nous appliquons l'héritage et remplaçons les méthodes :

Code Description
class Whale
{
public void printAll()
{
printColor();
printName();
}
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a whale");
}
}
Voici à quoi ressemble la classe Whale une fois que nous appliquons l'héritage et remplaçons la méthode. Nous ne savons rien au sujet d'une ancienne méthode printName.

3) Conversion de types.

Voici un point encore plus intéressant. Comme une classe hérite de toutes les méthodes et données de la classe parente, un objet de cette classe peut être une variable référencée de la classe parente (et du parent du parent, etc., jusqu'à la classe Object). Prenons l'exemple suivant :

Code Description
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printColor();
}
L'écran affiche :
Je suis blanche.
public static void main(String[] args)
{
Cow cow = new Whale();
cow.printColor();
}
L'écran affiche :
Je suis blanche.
public static void main(String[] args)
{
Object o = new Whale();
System.out.println(o.toString());
}
L'écran affiche :
Whale@da435a.
La méthode toString() est héritée de la classe Object.

« Intéressant. Mais pourquoi on a besoin de ça ? »

« C'est une fonctionnalité précieuse. Tu comprendras plus tard qu'elle est même très, très précieuse. »

4) Liaison tardive (répartition dynamique).

Voici à quoi cela ressemble :

Code Description
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printName();
}
L'écran affiche :
Je suis une baleine.
public static void main(String[] args)
{
Cow cow = new Whale();
cow.printName();
}
L'écran affiche :
Je suis une baleine.

Note que ce n'est pas le type de la variable qui détermine la méthode printName spécifique que nous appelons (celle de la classe Cow ou celle de la Whale), mais plutôt le type d'objet référencé par la variable.

La variable Cow stocke une référence à un objet Whale, et la méthode printName définie dans la classe Whale est appelée.

« Eh bien, on ne peut pas dire qu'ils ont ajouté ça par souci de clarté. »

« Oui, ce n'est pas évident. Retiens cette règle importante : »

L'ensemble de méthodes que tu peux appeler sur une variable est déterminé par le type de la variable. Mais la méthode/l'implémentation spécifique appelée est déterminée par le type/la classe de l'objet référencé par la variable.

« Je vais essayer. »

« C'est quelque chose qui reviendra constamment, donc tu comprendras rapidement et ne l'oublieras jamais. »

5) Conversion de types.

La conversion fonctionne différemment pour les types de référence, c'est-à-dire pour les classes, que pour les types primitifs. Cependant, les conversions avec élargissement et réduction s'appliquent également aux types de référence. Prenons l'exemple suivant :

Conversion avec élargissement Description
Cow cow = new Whale();

Une conversion avec élargissement classique. Maintenant, tu peux appeler uniquement les méthodes définies dans la classe Cow sur l'objet Whale.

Le compilateur te permettra d'utiliser la variable cow seulement pour appeler les méthodes définies par le type Cow.

Conversion avec réduction Description
Cow cow = new Whale();
if (cow instanceof Whale)
{
Whale whale = (Whale) cow;
}
Une conversion avec réduction classique avec un contrôle de type. La variable cow de type Cow stocke une référence à un objet Whale.
Nous vérifions que c'est le cas, puis nous effectuons la conversion de type (avec réduction). C'est ce qu'on appelle aussi le transtypage.
Cow cow = new Cow();
Whale whale = (Whale) cow; //exception
Tu peux également effectuer une conversion avec réduction d'un type de référence sans vérification du type de l'objet.
Dans ce cas, si la variable cow pointe vers autre qu'un objet Whale, une exception (InvalidClassCastException) sera levée.

6) Et maintenant, quelque chose de savoureux. L'appel de la méthode d'origine.

Parfois, lors du remplacement d'une méthode héritée, tu ne souhaiteras pas l'effacer entièrement. Il se peut que tu veuilles seulement lui ajouter un petit quelque chose.

Dans ce cas, tu voudrais que le code de la nouvelle méthode appelle la même méthode, mais sur la classe de base. Et Java te permet de le faire. Voici comment : super.method().

Voici quelques exemples :

Code Description
class Cow
{
public void printAll()
{
printColor();
printName();
}
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a cow");
}
}

class Whale extends Cow
{
public void printName()
{
System.out.print("This is false: ");
super.printName();

System.out.println("I'm a whale");
}
}
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printAll();
}
L'écran affiche :
Je suis blanche
Ceci est faux : Je suis une vache
Je suis une baleine

« Hmm. Eh bien, c'était une sacrée leçon. Mes oreilles robotiques ont failli fondre. »

« C'est vrai, ce n'est pas quelque chose de simple. Ça fait partie des éléments les plus difficiles que tu rencontreras. Le professeur a promis de te fournir des liens vers du matériel d'autres auteurs, comme ça si tu ne comprends toujours pas quelque chose, tu peux combler les manques. »