Table des matières
Abstract et java
“Le concret c'est de l'abstrait rendu familier par l'usage.” (Paul Longevin)
Nous voyons aujourd'hui dans ce TP guidé un concept qui porte malheureusement bien souvent son nom: abstract.
En effet, très souvent, les débutants en java et plus généralement en objet ne comprennent pas à quoi il sert, et donc l'utilisent à tord et à travers.
Heureusement, dans 30 minutes maximum, vous constaterez que c'est en réalité très simple!
C'est parti!
Un peu de théorie histoire de bien situer la problématique
Plus abstraite est la vérité que tu veux enseigner, plus tu dois en sa faveur séduire les sens. (Friedrich Nietzsche)
Je vais commencer par essayer de vous séduire avec un peu de théorie de la programmation orientée objet (certain diront que c'est pas gagné d'avance, mais j'aimerais bien voir Nietzsche à ma place ).
Vous savez normalement ce qu'est l'héritage. Ce concept est directement lié à l'utilisation de abstract.
Or, bien souvent, il est délicat d'imaginer dès le départ tous les comportements possibles dans une hiérarchie. De plus, les classes mères de haut niveaux ne sont pas toujours utiles en tant qu'instance, et servent juste à regrouper leurs héritiers.
Ces classes mères permettent avant tout de donner une direction générale à la programmation, au modèle, mais ne peuvent pas toujours tout prévoir.
Nous savons qu'il est possible de redéfinir des comportements (par exemple, toString(), equals(), etc.).
Mais pour l'instant, nous ne savons pas encore comment contraindre les héritiers à préciser leur comportement en redéfinissant des méthodes.
Nous allons voir ici que justement, la programmation objet permet de résoudre ces problèmes.
Nous allons découvrir trois manières de traiter l'abstraction en programmation orientée objet, et plus précisément avec java:
- les classes abstraites;
- les méthodes virtuelles (ou abstraites);
- les classes virtuelles pures (ou interface en java).
Passons à la pratique!
Prenons un premier exemple simple: mon sujet d'étude est la représentation d'Animaux dans un Zoo (ou un centre SPA si vous préférez). L'objectif est de décrire tous les types d'Animaux du zoo, de les faire jouer entre eux et de les faire manger.
Voici une première modélisation possible de la hiérarchie des animaux:
Notons tout de suite les règles suivantes:
- Un Chat ne joue pas avec un Chien
- Un Chien joue avec tout le monde
- Une Souris ne joue pas avec les autres espèces: elle n'est pas super sociable
- Un Chat mange des croquettes et des Souris
- Un Chien mange des croquettes
- Une Souris mange n'importe quel aliment, sauf des souris! (Il existe des souris cannibales, mais elles ne sont pas acceptées au Zoo).
La méthode jouer de Animal prend en paramètre un Animal a, et renvoie true si a peut jouer avec l'instance d'Animal sur laquelle est appelée la méthode.
Ainsi, avec le code suivant:
Animal chien = new Chien("Médor"); Animal chat = new Chat("Felix"); System.out.println(chien.jouer(chat));//true (un chien joue avec tout le monde) System.out.println(chat.jouer(chien));//false (un chat ne joue pas avec un chien)
La méthode jouer n'est donc pas “commutative”.
Voici une première version du code de notre programme:
- Animal.java
/** Une toute petite classe représentant un Animal @author Bruno Mascret */ public class Animal{ /**le nom de l'animal*/ private String nom =""; /** l'espèce de l'animal*/ private String espece=""; /** Constructeur d'Animal @param n valeur pour nom @param e valeur pour espece */ public Animal(String n, String e){ nom = n; espece = e; } /** Constructeur d'Animal L'animal est d'espèce inconnue @param n valeur pour nom */ public Animal(String n){ nom = n; espece = "inconnue"; } /** Pour faire jouer l'Animal. Affiche un petit message en console. @param a l'animal qui veut jouer avec this @return true si l'animal veut bien jouer avec a */ public boolean jouer(Animal a){ boolean ret = true; System.out.println("Je joue et c'est vraiment l'éclate"); return ret; } public void manger(){ System.out.println("Je mange"); } }
- Chien.java
/** Une toute petite classe représentant un Chien @author Bruno Mascret */ public class Chien extends Animal{ /** Constructeur de Chien @param n valeur pour nom */ public Chien(String n){ super(n,"chien"); } }
- Chat.java
/** Une toute petite classe représentant un Chat @author Bruno Mascret */ public class Chat extends Animal{ /** Constructeur de Chat @param n valeur pour nom */ public Chat(String n){ super(n,"chat"); } }
- Souris.java
/** Une toute petite classe représentant une Souris @author Bruno Mascret */ public class Souris extends Animal{ /** Constructeur de Souris @param n valeur pour nom */ public Souris(String n){ super(n,"S"); } }
Les classes abstraites
Dans la hiérarchie que je viens de donner, il n'est pas très utile d'utiliser le constructeur Animal: en effet, la classe Animal sert uniquement à regrouper ce qu'il y a de commun entre des héritiers (Chat, Chien, Souris, etc.).
Mais rien n'empêche pourtant d'utiliser le constructeur Animal pour l'instant. Imaginez que vous travaillez à 50 sur ce programme, et qu'un petit rigolo (qui n'a pas assisté aux réunions de modélisation) s'amuse à créer des instances d'Animal avec ce constructeur: ces instances ne sont pas utilisables et ne représentent concrètement rien d'utile dans notre problème.
Hé bien voilà une première utilisation possible d'abstract: nous allons transformer Animal en classe abstraite, autrement dit interdire l'utilisation du constructeur Animal avec l'opérateur new.
Notez bien la subtilité: le constructeur Animal existe toujours: il peut être utilisé explicitement avec super(nom,espece) dans un héritier, ou implicitement si Animal()existait (ce n'est pas le cas dans notre exemple).
C'est son usage avec new qui est interdit.
Pour rendre la classe Animal abstraite, il suffit d'ajouter abstract devant la déclaration class:
/** Une toute petite classe représentant un Animal @author Bruno Mascret */ public abstract class Animal{ //le code de la classe }
Dans un modèle UML semi-graphique, le nom d'une classe abstraite est écrit en italique:
Voici un exemple de code illustrant les constructions possibles, et qui finit par une erreur de compilation:
/** Classe de test pour le TP sur abstract @author Bruno Mascret */ public class TestAbstract{ /** Méthode principale de test */ public static void main(String [] trucs){ Animal dog1 = new Chien("Médor"); Chien dog2 = new Chien("Milou"); Chat cat1 = new Chat("Félix"); Animal cat2 = new Chat("Tom"); Souris mouse1 = new Souris("Jerry"); Animal mouse2 = new Souris("Mickey"); //Animal horse1 = new Animal("Jolly","Cheval"); } }
La dernière ligne produira l'erreur suivante dès la compilation si vous la décommentez:
TestAbstract.java:36: error: Animal is abstract; cannot be instantiated Animal horse1 = new Animal("Jolly","Cheval");
On voit bien que c'est l'utilisation du constructeur Animal avec new qui rend impossible la création directe d'instances d'Animal.
Allons un peu plus loin: les méthodes virtuelles (ou abstraites)
Nous avons vu que nous pouvions interdire la création directe d'instances d'une classe lorsque celle-ci n'est pas “intéressante” en tant qu'objet, mais utile pour des raisons de regroupement hiérarchique.
Il est tout à fait possible d'étendre ce raisonnement aux méthodes: dans notre exemple, nous savons que la méthode jouer est intéressante pour un Animal, mais il est très difficile d'écrire son code dans la classe Animal directement. En plus, chaque fois qu'un nouveau type d'Animal serait créé, il faudrait modifier directement cette méthode dans la classe Animal. Ceci rendrait très difficile la constitution de bibliothèques ou d'API.
Il serait bien plus pratique de pouvoir obliger les héritiers à redéfinir cette méthode:
- d'une part, on serait sûrs que tous les animaux ont bien une méthode jouer;
- d'autre part, on serait sûr que tout héritier d'Animal a bien une méthode jouer qui correspond à son comportement.
Hé bien c'est possible! Nous allons déclarer jouer comme étant une méthode virtuelle dans la classe Animal.
Les méthodes virtuelles ou méthodes abstraites
Une méthode virtuelle:
- n'a pas de corps! (elle ne contient pas de code, c'est juste un prototype);
- est dessinée en italique sur les modèles UML semi-graphiques;
- en java, on utilise là-encore le mot-clef abstract pour définir une méthode virtuelle.
Voici le nouveau diagramme de classe:
Et voici le code correspondant à cette modification. Il implémente la fonctionnalité demandée sur le comportement “jouer” pour chaque Animal.
- Animal.java
/** Une toute petite classe représentant un Animal @author Bruno Mascret */ public abstract class Animal{ /**le nom de l'animal*/ private String nom =""; /** l'espèce de l'animal*/ private String espece=""; /** Constructeur d'Animal @param n valeur pour nom @param e valeur pour espece */ public Animal(String n, String e){ nom = n; espece = e; } /** Constructeur d'Animal L'animal est d'espèce inconnue @param n valeur pour nom */ public Animal(String n){ nom = n; espece = "inconnue"; } /** Pour faire jouer l'Animal. Affiche un petit message en console. @param a l'animal qui veut jouer avec this @return true si l'animal veut bien jouer avec a */ public abstract boolean jouer(Animal a); /*pas de corps pour une méthode abstraite { boolean ret = true; System.out.println("Je joue et c'est vraiment l'éclate"); return ret; }*/ public void manger(){ System.out.println("Je mange"); } }
- Chien.java
/** Une toute petite classe représentant un Chien @author Bruno Mascret */ public class Chien extends Animal{ /** Constructeur de Chien @param n valeur pour nom */ public Chien(String n){ super(n,"chien"); } /** Pour faire jouer le chient. Affiche un petit message en console. @param a l'animal qui veut jouer avec this @return true si l'animal veut bien jouer avec a */ public boolean jouer(Animal a){ System.out.println("Je joue avec tout le monde moi!"); return true; } }
- Chat.java
/** Une toute petite classe représentant un Chat @author Bruno Mascret */ public class Chat extends Animal{ /** Constructeur de Chat @param n valeur pour nom */ public Chat(String n){ super(n,"chat"); } /** Pour faire jouer le chat. Affiche un petit message en console. @param a l'animal qui veut jouer avec this @return true si l'animal veut bien jouer avec a */ public boolean jouer(Animal a){ boolean ret = true; String message = "Je veux bien jouer avec les instances de " + a.getClass().getName() ; if(a instanceof Chien){ ret=false; message = "Je ne joue pas avec les chiens, moi!"; } System.out.println(message); return ret; } }
Notez au passage l'utilisation de getClass() qui renvoie une instance de la classe Class.
La méthode toString() de la classe Class renvoie une chaîne du type “class NomDeLaClasse”.
La méthode getName() de la classe Class renvoie le nom de la classe uniquement.
Pour plus d'info: la class Class dans l'API java
- Souris.java
/** Une toute petite classe représentant une Souris @author Bruno Mascret */ public class Souris extends Animal{ /** Constructeur de Souris @param n valeur pour nom */ public Souris(String n){ super(n,"S"); } /** Pour faire jouer la souris. Affiche un petit message en console. @param a l'animal qui veut jouer avec this @return true si l'animal veut bien jouer avec a */ public boolean jouer(Animal a){ boolean ret = false; String message = "Je ne joue pas avec les instances de " + a.getClass().getName() ; if(a instanceof Souris){ ret=true; message = "Je joue qu'avec mes copines souris, alors oui!"; } System.out.println(message); return ret; } }
- TestAbstract.java
/** Classe de test pour le TP sur abstract @author Bruno Mascret */ public class TestAbstract{ /** Méthode principale de test */ public static void main(String [] trucs){ //test des Animal dog1 = new Chien("Médor"); Chien dog2 = new Chien("Milou"); Chat cat1 = new Chat("Félix"); Animal cat2 = new Chat("Tom"); Souris mouse1 = new Souris("Jerry"); Animal mouse2 = new Souris("Mickey"); //Animal horse1 = new Animal("Jolly","Cheval"); /* Tests du comportment jouer */ boolean d1 = dog1.jouer(dog2); boolean d2 = dog1.jouer(cat1); boolean d3 = dog1.jouer(mouse1); boolean c1 = cat1.jouer(dog2); boolean c2 = cat1.jouer(cat2); boolean c3 = cat1.jouer(mouse1); boolean s1 = mouse1.jouer(dog2); boolean s2 = mouse1.jouer(cat2); boolean s3 = mouse1.jouer(mouse2); System.out.println("Résultat pour dog1:"+d1+"; "+d2+"; "+d3); System.out.println("Résultat pour cat1:"+c1+"; "+c2+"; "+c3); System.out.println("Résultat pour mouse1:"+s1+"; "+s2+"; "+s3); } }
L'exécution de la méthode main de TestAbstract produira l'affichage suivant:
Je joue avec tout le monde moi! Je joue avec tout le monde moi! Je joue avec tout le monde moi! Je ne joue pas avec les chiens, moi! Je veux bien jouer avec les instances de Chat Je veux bien jouer avec les instances de Souris Je ne joue pas avec les instances de Chien Je ne joue pas avec les instances de Chat Je joue qu'avec mes copines souris, alors oui! Résultat pour dog1:true; true; true Résultat pour cat1:false; true; true Résultat pour mouse1:false; false; true
Et si je ne veux pas redéfinir une méthode abstraite, hein?
Vous vous posez peut-être cette question.
Hé bien, c'est possible! Mais pas à n'importe quel prix!
Les chats sont paresseux, supposons que la classe Chats ne redéfinisse pas la méthode jouer.
Que va dire le compilateur? Essayons pour voir! Commentez la méthode jouer dans Chat
/** Pour faire jouer le chat. Affiche un petit message en console. @param a l'animal qui veut jouer avec this @return true si l'animal veut bien jouer avec a */ /* Je veux pas redéfinir moi! public boolean jouer(Animal a){ boolean ret = true; String message = "Je veux bien jouer avec les instances de " + a.getClass().getName() ; if(a instanceof Chien){ ret=false; message = "Je ne joue pas avec les chiens, moi!"; } System.out.println(message); return ret; } */
Recompilons:
./Chat.java:22: error: Chat is not abstract and does not override abstract method jouer(Animal) in Animal public class Chat extends Animal{
Apparemment, le compilateur semble contrarié. Regardons en détail ce qu'il nous raconte:
“Chat n'est pas abstract” → ok
“…et ne redéfinit pas la méthode abstraite jouer(Animal)” → on vient de la commenter…
Quel problème cela pose-t-il pour le compilateur: raisonnons par l'absurde.
Si le compilateur permettait à Chat de ne pas redéfinir la méthode jouer, et également de construire des instances de Chat, que se passerait-il lorsque l'on voudrait appeler la méthode jouer(Animal) sur une instance de Chat?
Il n'y aurait pas de code défini → le programme ne saurait pas quoi faire.
Le “contrat” donné par Animal est que toute instance d'Animal doit pourvoir jouer et manger.
Donc, si ce comportement n'est pas définit dans un héritier d'Animal (Chat), alors il n'est pas possible de créer des instances de Chat.
Le compilateur propose donc deux solutions:
- soit passer la classe Chat en classe abstraite, afin d'empêcher l'utilisation du constructeur Chat directement;
- soit redéfinir la méthode jouer dans Chat, pour permettre la construction d'instances de Chat.
Si on choisit la première solution, Chat devient abstract et il n'y a plus de problème de compilation… à condition de ne pas utiliser le constructeur Chat avec new.
Si une classe hérite de Chat (par exemple ChatJoueur) et que le programmeur veut créer des instances de ChatJoueur, il devra alors redéfinir la méthode jouer héritée de Animal par l'intermédiaire de Chat dans la classe ChatJoueur: on appelle ça “refiler la patate chaude”.
Pour aller encore plus loin: les classes virtuelles pures (ou interfaces en java)
→ ce dernier point fait l'objet d'un autre TP guidé…