Partie C : Classes et objets
Objectif
Être capable d’implémenter une application comportant plusieurs classes, y compris des classes abstraites, reliées par héritage, ainsi que des interfaces, et des itérateurs, en maîtrisant l’implémentation et le comportement des constructeurs, de méthodes redéfinies avec liaisons dynamiques. Être capable de prévoir le comportement d’un tel programme, y compris les objets et données créés dans la mémoire de l’ordinateur (pile, tas, zone des données statiques).
Prérequis
Parties A et B
C1 : Exemple introductif
On appelle classe dérivée d’une classe S toute classe D qui hérite de S dans le sens suivant :
-
Toutes les variables de classe et attributs de S appartiennent implicitement à D.
-
Toutes les méthodes de S appartiennent à D, mais peuvent être redéfinies dans D.
-
D peut posséder des variables d’instance, de classe, des méthodes d’instance, de classe, spécifiques qui n’appartiennent pas à S.
Les redéfinitions d’attributs, variables de classe, méthodes de classes ne seront pas abordées.
Nous allons découvrir ces notions avec un exemple qui commence par la définition d’une classe Personne, qui représente un individu, et d’une classe Document, qui représente un document ayant un nom et un ou plusieurs auteurs (qui sont des instances de Personne).
Pour l’instant il n’y a pas d’héritage, mais seulement un lien de composition entre Document et Personne.
public class Personne
{
private String nom;
private Date dateNaiss;
public Personne(String nom)
{
// DateNaiss est initialisé implicitement à null
this.nom=nom;
}
public Personne(String nom, Date naiss)
{
this(nom); // Appel du constructeur à 1 paramètre
this.dateNaiss = naiss;
}
public String toString()
{
String s = nom;
if(dateNaiss!=null)
{
s = s + "né le " + dateNaiss;
}
return s;
}
}
public class Document
{
private String nom;
private Personne[] auteurs;
public Document(String n, Personne[] a)
{
this.nom=n; auteurs = a;
}
public int nbAuteurs()
{
return auteurs.length;
}
public String toString()
{
String da;
if(nbAuteurs()==0) da="inconnu";
else
{
da="";
for (int i = 0; i < nbAuteurs(); i++)
da = da + auteurs[i] + " ";
}
return nom + "\nAuteur(s) : "+da;
}
}
Les deux classes peuvent être représentées par ce schéma appelé diagramme de classes.
La notion de composition correspond à l’auxiliaire avoir : un document a un ou plusieurs auteurs. A présent nous allons introduire une classe Livre qui représente un cas particulier de document : un livre est un document. Cet auxiliaire être traduit la notion d’héritage.
public class Livre extends Document
{
private long isbn;
private int annee;
public Livre(String nom, Personne[] aut, long isbn, int apub)
{
super(nom, aut); // Appel du constructeur de Document
this.isbn=isbn;
annee=apub;
}
public long getIsbn()
{
return isbn;
}
public String toString()
{
String s = "Livre : "+super.toString(); // Appel toString de document
return s+"\nISBN : "+isbn+" ("+annee+")";
}
}
Le mot-clé extends traduit l’héritage. La classe Livre est dérivée de Document. Document est la superclasse de Livre.
Le code de tout constructeur de la classe dérivée doit commencer par l’appel d’un constructeur de sa superclasse à l’aide du mot clé super
.
La méthode toString
existe déjà dans la superclasse et elle est ici redéfinie. La méthode toString
de la superclasse est appelée depuis la classe dérivée grâce à la syntaxe super.toString()
.
Voici le diagramme de classe complet. Toute instance de Livre est une instance de Document. Toute instance de Document a des auteurs qui sont des instances de Personne. Donc, évidemment, toute instance de Livre a des auteurs…
Il y a quelques règles à connaitre en matière d’héritage et de constructeurs :
- Si une classe n’a pas de constructeur explicite alors Java lui ajoute un constructeur implicite qui appelle le constructeur par défaut de sa superclasse, et initialise ses variables d’instance avec des valeurs par défaut.
- Si une classe a au moins un constructeur explicite alors Java ne produit pas de constructeur par défaut implicite pour cette classe, mais on peut bien sûr lui donner un constructeur par défaut explicite.
- Si le code d’un constructeur explicite d’une classe dérivée ne commence pas par super(…) alors ce constructeur appelle implicitement le constructeur par défaut de sa superclasse. Et si la superclasse n’a pas de constructeur par défaut, le programme ne compile pas !
- Si le code d’un constructeur explicite d’une classe dérivée appelle explicitement un constructeur de sa superclasse, son code doit commencer par super(…).
Pour mémoire, rappelons le sens de quelques termes importants.
- implicite : produit automatiquement pas le compilateur, non visible dans le code.
- explicite : écrit par le programmeur.
- constructeur par défaut : sans paramètres.
- valeur par défaut : placée automatiquement dans une variable qui n’est pas initialisée explicitement.
Exercices de découverte
C-dec-10
Complétez ces lignes de code permettant de créer une instance de Document
représentant un document ayant un seul auteur nommé N. Wirth et ayant pour titre "Algorithm + Data Structure = Program", puis d’afficher la description de ce document produite par la méthode toString
.
Personne[] aut = {/* création d'une instance de Personne */};
Document doc = new /* À compléter */;
System.out.println(/* À compléter */);
L’instance de Personne
doit être créée avec le constructeur à un paramètre, qui ne nécessite pas la date de naissance.
Vous pouvez utiliser un ordinateur pour rechercher la bonne syntaxe par essai et erreurs.
Personne[] aut = {new Personne("N. Wirth")};
Document doc = new Document("Algorithm + Data Structure = Program", aut);
System.out.println(doc);
C-dec-11
Complétez cette ligne de code permettant de créer une instance de Livre
représentant un document ayant un seul auteur nommé N. Wirth et ayant pour titre "Algorithm + Data Structure = Program", un numéro isbn
égal à 130224189, ayant été publié en 1976.
Personne[] aut = {new Personne("N. Wirth")};
Livre w = new Livre (// À compléter //);
Livre w = new Livre
("Algorithm + Data Structure = Program",
aut, 130224189, 1976
);
C2 : Héritage et types
En Java, chaque variable et chaque paramètre a un type. Nous avons vu qu’il existe des types primitifs : int
, boolean
, double
, long
. Chaque nom de classe représente aussi un type. Mais du fait de l’héritage, une instance de classe peut avoir plusieurs types !
Voici un premier exemple d’utilisation de notre classe Livre
…
Livre w = new Livre(
"Patience dans l'azur",
new Personne[]{new Personne("Hubert Reeves")},
2020099179, 1981);
System.out.println(w.toString());
System.out.println(w.getIsbn());
…qui produit l’affichage :
Livre : Patience dans l'azur
Auteur(s) : Hubert Reeves
ISBN : 2020099179 (1981)
2020099179
Maintenant, il est aussi possible d’écrire…
Document w = new Livre(
"Patience dans l'azur",
new Personne[]{new Personne("Hubert Reeves")},
2020099179, 1981);
System.out.println(w.toString());
…car toute instance de Livre
est aussi par héritage une instance de Document
. Mais… Il n’est plus possible d’appeler la méthode getIsbn
.
System.out.println(w.getIsbn()); // Ne compile pas
Parce que c’est une méthode de Livre
qui n’existe pas dans Document
, et que la variable w
est de type Document
. Pas de problème avec la méthode toString
qui est redéfinie dans la classe Livre
mais existe dans Document
et, cerise sur le gâteau, c’est la méthode toString
de Livre
qui sera exécutée et non celle de Document
.
Ce petit miracle s’appelle la liaison dynamique et nous y reviendrons plus tard. Observons ce qui se produit en mémoire.
Exercices de découverte
C-dec-20
Donnez les lignes de code permettant, après la déclaration précédente,
- d’afficher le nombre d’auteurs du livre,
- d’afficher le numéro isbn du livre,
- d’afficher la description de ce document produite par la méthode
toString
de la classeLivre
.
System.out.println(w.nbAuteurs());
System.out.println(w.getIsbn());
System.out.println(w.toString());
C-dec-21
On écrit les lignes suivantes.
Personne[] aut = {new Personne("N. Wirth")};
Livre w = new Livre
("Algorithm + Data Structure = Program",
aut, 130224189, 1976
);
Document d = w; // ligne A
System.out.println(d); // ligne B
System.out.println(d.nbAuteurs()); // ligne C
System.out.println(d.getIsbn()); // ligne D
Toutes les lignes passent à la compilation sauf la dernière, qui comporte une erreur. Expliquez pourquoi.
Toute instance de Livre
est aussi, par héritage, une instance de Document
, et c’est pourquoi sa référence peut être mise dans une variable de type Document
. Les méthodes nbAuteurs
et toString
étant définies dans la classe Document
, elles sont accessibles via la variable d
.
Par contre, la méthode getIsbn
n’est pas définie dans la classe Document
et donc pas accessible via une variable de type Document
, quand bien même cette variable contient une instance de Livre
.
C-dec-21
Quand on exécute le code de l’exercice précédent (en supprimant la dernière ligne puisqu’elle est incorrecte), on obtient l’affichage suivant :
Livre : Algorithm + Data Structure = Program
Auteur(s) : N. Wirth
ISBN : 130224189 (1976)
1
Bien que la variable d
soit de type Document
, c’est la méthode toString
de la classe Livre
qui a été exécutée. Tentez de trouver une explication.
La méthode toString
est définie dans Document
et redéfinie dans Livre
. Comme elle est définie dans Document
, elle est donc accessible via la variable d
de type Document
. Mais comme cette variable pointe une instance de Livre
(une classe dérivée de Document
), c’est la méthode toString
de Livre
qui est exécutée. Si la méthode n’avait pas été redéfinie dans Livre
, c’est celle de Document
qui aurait été exécutée.
Ce comportement n’est pas évident à prévoir. Il résulte d’un choix faite par les inventeurs de la programmation orientée objet. Si vous avez eu du mal à deviner la réponse, refaites les exercices de découverte dans quelques jours.
C3 : Compléments sur l’héritage et la structuration des programmes
Notion de package
Un package regroupe un ensemble de classes. Le fichier dans lequel est définie une classe d’un package p doit comporter, au début, la déclaration :
package p;
Si une classe C
utilise une classe U
située dans un package q
différent de celui où elle se trouve, une des déclarations suivantes doit apparaître avant la définition de C
:
import q.U; // Importe la classe U du package q
import q.*; // Importe toutes les classes du package q
Droits d’accès
Jusqu’à maintenant, nous avons utilisé deux droits d’accès : public et private. Il en existe deux autres : protected et « par défaut ». Ce dernier s’applique lorsque le droit d’accès à un élément n’est pas précisé.
- public : L’élément est accessible depuis toutes les classes du programme.
- private : L’élément est accessible seulement depuis la classe où il est défini ou déclaré.
- protected : L’élément est accessible depuis les classes dérivées de la classe où il est défini et déclaré et depuis toutes les classes du même package.
- par défaut : L’élément est accessible seulement depuis les classes du package de la classe où il est défini ou déclaré.
Les classes ont elles-mêmes des droits d’accès. Une classe directement définie dans un fichier source .java
, comme nous l’avons fait jusqu’à présent, peut avoir le droit public ou « par défaut ». Une classe public est utilisable dans tout le programme où elle est définie, une classe avec droit d’accès par défaut n’est utilisable que depuis le package où elle est définie.
Hiérarchie de classe
Une classe déclarée final ne peut avoir de classe dérivée. Mais en dehors de cette restriction, toute classe dérivée peut elle-même avoir des classes dérivées. D’ailleurs toutes les classes descendent d’une classe définie par Java nommée Object. Par contre Java ne permet pas l’héritage multiple : chaque classe (excepté Object) a une seule superclasse.
Or il se trouve que la classe Object possède nativement certaines méthodes, dont notamment la méthode toString. On peut donc utiliser cette méthode héritée depuis n’importe quelle classe, même si elle n’y est pas redéfinie.
Exercices de découverte
C-dec-30
On ajoute la méthode suivante à la classe Document
.
private void test()
{
System.out.println("Exécution de test dans la classe Document");
}
On ajoute la méthode suivante à la classe Livre
.
public void test()
{
System.out.println("Exécution de test dans la classe Livre");
}
Le programme compile. Que peut-on en déduire en matière de possibilité de redéfinition d’une méthode private
dans une classe dérivée ?
Non seulement on peut redéfinir une méthode private
, mais on peut rendre public
sa redéfinition. En fait cela se généralise : on peut toujours redéfinir une méthode avec des droits d’accès moins restrictifs.
C-dec-31
On ajoute la méthode suivante à la classe Document
.
public void test()
{
System.out.println("Exécution de test dans la classe Document");
}
On ajoute la méthode suivante à la classe Livre
.
private void test()
{
System.out.println("Exécution de test dans la classe Livre");
}
Le compilateur indique une erreur. Que peut-on en déduire ?
Une méthode public
, ne peut être redéfinie avec un droit d’accès private
. En fait cela se généralise : on ne peut redéfinir une méthode avec des droits d’accès plus restrictif que ceux dont elle dispose dans la superclasse.
C-dec-32
On ajoute la méthode suivante à la classe Document
.
private void test()
{
System.out.println("Exécution de test dans la classe Document");
}
On ajoute la méthode suivante à la classe Livre
.
private void test()
{
super.test();
System.out.println("Exécution de test dans la classe Livre");
}
Le programme va-t-il compiler sans erreur ? Si oui, que se produira-t-il à l’exécution de test
avec une instance de Livre
? Si non, expliquez l’erreur.
Il y a une erreur car la méthode test
de Document
est privée, donc elle ne peut être appelée par aucune méthode de Livre
.
C4 : Interface
Une interface contient des signatures de méthodes et des commentaires expliquant ce que sont censées faire ces méthodes.
Par exemple, l’interface List<T>
est implémentée par les classes ArrayList<T>
et LinkedList<T>
.
Autre exemple : il existe une interface prédéfinie Comparable<T>
dans laquelle est déclarée une méthode int compareTo(T e)
permettant de comparer deux objets de type T
(l’objet courant et l’objet e
) en respectant une relation d’ordre sur les objets de type T
. Cette méthode doit retourner un entier négatif si this
< e
, un entier positif si this
> e
, et en cas d’égalité elle doit retourner 0.
Une interface définit un type. Par exemple, on peut déclarer une variable de type List
.
Une interface ne contient pas de code et ne possède pas de constructeur.
Une classe peut implémenter une ou plusieurs interfaces. Elle doit alors implémenter toutes les méthodes déclarées dans cette interface (en respectant les spécifications données dans les commentaires de l’interface).
Voici un exemple d’utilisation de l’interface Comparable
. Cette interface est déjà prédéfinie dans le langage Java, sous la forme suivante :
public interface Comparable<T>
{
public int compareTo(T e);
}
Pour rendre une classe comparable, on fait en sorte qu’elle implémente cette interface :
public class Date implements Comparable<Date>
{
// ...
public int compareTo(Date d)
{
if(d.annee!=annee)
return this.annee-d.annee;
else if(d.mois!=mois)
return this.mois-d.mois;
else
return this.jour-d.jour;
}
}
A présent, nous pouvons créer une liste de dates et utiliser la méthode statique sort
de la classe prédéfinie Collections
pour trier cette liste sans avoir à coder un algorithme de tri.
public static void test()
{
List<Date> w = new ArrayList<>();
w.add(new Date(25,1,1962));
w.add(new Date(26,1,1962));
w.add(new Date(25,2,1961));
Collections.sort(w);
System.out.println(w);
}
L’affichage produit par l’exécution nous permet de vérifier que le tri a bien été exécuté.
[25/2/1961, 25/1/1962, 26/1/1962]
L’appel Collections.sort(w)
fonctionne si et seulement si w
désigne une instance d’une classe qui implémente l’interface List
représentant une liste dont les éléments sont des instances d’une classe qui implémente l’interface Comparable
. C’est un peu compliqué, mais très puissant puisque cela évite d’avoir à programmer une méthode de tri lorsque les conditions sont réunies.
Exercices de découverte
C-dec-40
Relisez le code de la classe Personne
. Si on décide que cette classe doit implémenter l’interface Comparable
, que doit-on ajouter à cette classe ? À quoi cela pourrait servir ? Proposez un exemple d’utilisation.
On doit ajouter dans la classe Personne
une méthode compareTo
permettant de comparer l’instance courante avec une autre instance. Cette comparaison pourrait porter sur les noms, en étant basée sur l’ordre alphabétique, mais pourrait aussi être basée sur les dates de naissances, à condition de disposer d’un moyen de comparer des instances de Date
. (Il serait alors judicieux que la classe Date
implémente aussi l’interface Comarable
.)
Cela permettrait, par exemple de trier des listes d’instances de Personne
en utilisant la méthode Collection.sort
.
C5 : Classes et méthodes abstraites
Jusqu’à maintenant, nous avons parlé de classes, dans lesquelles toute méthode non héritée doit être implémentée (codée), et d’interfaces, dans lesquelles des méthodes sont déclarées, mais pas implémentées.
Une classe abstraite est une sorte d’intermédiaire entre ces deux concepts : certaines méthodes peuvent être implémentées alors que d’autres, appelées méthodes abstraites, sont juste déclarées. Une classe abstraite peut comporter des variables d’instances et des variables de classe.
Une classe abstraite ne peut pas être instanciée.
Une classe abstraite n’a d’intérêt que si elle a au moins une classe dérivée « concrète », qui implémente ses méthodes abstraites.
Fait remarquable : Une méthode concrète peut très bien appeler une méthode abstraite ! Le code exécuté est celui de l’implémentation de cette méthode dans une classe dérivée qui est déterminée lors de l’exécution du programme. On parle alors de liaison dynamique.
Pour bien comprendre le principe de la liaison dynamique, nous allons développer un exemple dans lequel une classe abstraite Animal
a deux classes dérivées concrètes : Chat
et Elephant
.
La classe abstraite Animal
a une variable d’instance nom
et un constructeur qui initialise cette variable. Elle a une méthode abstraite reactionDanger
et une méthode concrète dangerDetecte
qui appelle la méthode abstraite reactionDanger
.
La méthode abstraite reactionDanger
devra être implantée dans les classes Chat
et Elephant
.
public abstract class Animal
{
private String nom;
public Animal(String nom)
{
this.nom = nom;
}
public abstract void reactionDanger(); // Méthode abstraite
public void dangerDetecte() // Méthode concrète
{
System.out.println(nom + " a détecté un danger.");
reactionDanger(); // Appel d'une méthode abstraite
}
}
On voit dans cet exemple que la méthode concrète dangerDetecte
de la classe Animal
appelle une méthode abstraite reactionDanger
de cette même classe. Lors de l’exécution, c’est une implémentation concrète de reactionDanger
définie dans une classe dérivée qui sera appelée. C’est typiquement un cas de liaison dynamique.
Voici deux exemples de classes qui dérivent de la classe abstraite Animal
.
public class Chat extends Animal
{
public Chat(String n)
{
super("Le chat "+n);
}
public void reactionDanger()
{
System.out.println("Il miaule et prend la fuite");
}
}
public class Elephant extends Animal
{
public Elephant(String n)
{
super("L’éléphant "+n);
}
public void reactionDanger()
{
System.out.println("Il barrit et il charge !");
}
}
Voici un exemple d’utilisation des classes que nous venons de définir.
Animal felix = new Chat("Felix"); // ligne A
Animal jumbo = new Elephant("Jumbo"); // ligne B
felix.dangerDetecte(); // ligne C
jumbo.dangerDetecte(); // ligne D
L’exécution de la ligne C produit l’affichage "Le chat Felix a détecté un danger. Il miaule et prend la fuite. "
L’exécution de la ligne D produit l’affichage "L’éléphant Jumbo a détecté un danger. Il barrit et il charge ! "
Dans les deux cas, la méthode appelée, dangerDetecte
, est héritée de la superclasse Animal
et appelle la méthode reactionDanger
, abstraite dans la classe Animal
, mais implémentée dans les classes Chat
et Elephant
.
Les classes abstraites permettent notamment d’éviter la duplication de code : on y implémente des algorithmes qui sont utilisés ou utilisables par plusieurs classes dérivées concrètes.
Par exemple, la classe abstraite AbstractList<T>
de Java a parmi ses classes dérivées les classes ArrayList<T>
et (indirectement) LinkedList<T>
. Elle implante des algorithmes qui sont les mêmes pour ces deux classes, comme par exemple la méthode…
public boolean addAll(int i, Collection<? extends T> c)
…qui insère dans la liste courante, à partir de la position i, tous les éléments de c. Cette méthode addAll
utilise la méthode…
public void add(int i, T e)
…qui devra être implémentée dans toute classe concrète dérivant de AbstractList
.
Voici une vue partielle de la hiérarchie des classe java dédiées à la représentation des listes.
C-dec-50
Que se passe-t-il si on écrit, par exemple dans une méthode main
, la ligne suivante ?
Animal w = new Animal("Flipper le dauphin");
Il y a une erreur car on ne peut pas créer d’instance d’une classe abstraite.
C-dec-51
On ajoute une méthode enrouleTrompe
à la classe Elephant
.
public void enrouleTrompe() {...}
Que se passe-t-il si on écrit, par exemple dans une méthode main
, les lignes suivantes ?
Animal jumbo = new Elephant("Jumbo");
jumbo.enrouleTrompe();
Il y a une erreur car même si enrouleTrompe
est une méthode d’instance de la classe Elephant
, elle n’est pas définie dans Animal
donc pas accessible via la variable jumbo
.
C6 : Les itérateurs
Un itérateur est un objet (!) permettant de parcourir facilement tous les objets d’une collection, et en particulier d’une liste. En Java, toute classe implémentant l’interface prédéfinie Iterable<T>
permet d’utiliser des itérateurs. C’est le cas notamment des classes ArrayList
et LinkedList
, mais aussi des tableaux.
L’interface Iterable<T>
est indissociable d’une autre interface prédéfinie nommée Iterator<T>
.
public interface Iterator<T>
{
boolean hasNext(); // vrai s'il y a un élément suivant
T next(); // élément suivant
void remove(); // suppression de l’élément suivant
}
public interface Iterable<T>
{
Iterator<T> iterator()
// ...
}
Pour comprendre l’intérêt et la manière d’utiliser ces concepts, considérons l’exemple d’une instance de ArrayList<Integer>
.
ArrayList<Integer> w = new ArrayList<>();
w.add(15); w.add(12); w.add(33);
Iterator<Integer> scan = w.iterator(); // ligne A
while(scan.hasNext())
{
System.out.println(scan.next());
}
En ligne A, La méthode iterator
retourne un itérateur, c’est à dire une instance d’une classe qui implémente l’interface Iterator
.
Dans la boucle while
, Les méthodes hasNext
et next
de l’itérateur permettent de parcourir la liste et de récupérer ses éléments.
Cette boucle peut être réécrite avec une syntaxe plus lisible qui masque l’utilisation d’un itérateur et qui est la manière la plus courante de procéder.
for(int x : w)
{
System.out.println(x);
}
C-dec-60
Soit une classe C
représentant une collection d’objets (liste, ensemble…). Soit w
une instance de C
. À quelle condition peut-on utiliser la syntaxe suivante :
for(x : w) {...}
Il faut que la classe C
implémente l’interface Iterable<...>
(directement ou parce qu’elle dérive d’une classe qui implémente cette interface).
C7 : Classes internes
Les compétences relatives aux notions introduites ici ne seront pas évaluée et sont présentées à titre informatif.
Les classes internes anonymes
Il arrive parfois qu’on ait besoin de créer une classe qui dérive d’une classe existante ou qui implémente une interface, et que l’on ait besoin que d’une seule instance de cette classe. Puisqu’une seule instance est requise, il n’est pas nécessaire de donner un nom à la classe et il est possible de créer la classe et son unique instance en une seule ligne de programme. Voici un exemple dont l’unique intérêt est la simplicité.
abstract class Oiseau
{
abstract void voler();
}
public class Main
{
public static void main(String[] args)
{
Oiseau bird = new Oiseau()
{
void voler()
{
System.out.println("Flap flap");
}
};
bird.voler();
}
}
La classe Oiseau
est abstraite donc on ne peut pas créer directement avec new
une instance de cette classe.
La variable bird
de ma méthode main
reçoit une référence d’une instance d’une classe dérivée de Oiseau
, qui n’a pas de nom et dans laquelle la méthode voler
est implémentée. Cette classe dérivée n’aura qu’une seule instance.
Les classes internes statiques
On peut définir une ou plusieurs classes dans une classe comme dans l’exemple suivant :
public class Test
{
public static abstract class Animal
{
private String nom;
public Animal(String nom) {this.nom = nom;}
public abstract void reactionDanger();
public void dangerDetecte()
{
System.out.println(nom + " a detecte un danger.");
reactionDanger();
}
}
public static class Chat extends Animal
{
public Chat(String n) {super("Le chat "+n);}
public void reactionDanger()
{
System.out.println("Il miaule et prend la fuite");
}
}
}
Pour instancier la classe Chat
en dehors de la classe Test
, on doit utiliser la syntaxe illustrée par cet exemple.
Test.Animal felix = new Test.Chat("Felix");
felix.reactionDanger();
Les classes membres
Le principe est le même que pour les classes internes statiques, mais chaque instance de la classe interne est liée à une instance de la classe enveloppante, et à accès aux variables d’instance de la classe enveloppante. Dans cet exemple, la classe Liste
représente une liste chaînée dont les éléments de type double
, sont contenus dans des maillons, qui sont des instances d’une classe interne Maillon
.
public class Liste
{
Maillon deb;
private class Maillon
{
Maillon suiv;
double val;
Maillon(double val)
{
this.val=val;
suiv=deb;
deb=this;
}
}
public void ajoute(double x)
{
Maillon m = new Maillon(x);
}
public void print()
{
Maillon cur=deb;
while(cur!=null)
{
System.out.println(cur.val);
cur = cur.suiv;
}
}
}
Le constructeur de Maillon
a accès aux attributs de la classe Liste
.
La méthode ajoute
ajoute un élément en début de liste. La méthode print
affiche les éléments de la liste.
La classe Maillon
étant private
, elle ne peut être exploitée que dans la classe Liste
.
Exercices d’assimilation
C-ass-00
Réalisez une classe Video
qui représente un cas particulier de Document
. Les instances de Video
représentent des vidéos qui, en plus de tous les attributs d’un document, ont une durée. Cette durée doit être l’un des paramètres du constructeur de la classe Video
. La classe Video
doit redéfinir la méthode toString
de sorte que cette méthode retourne la description retournée par la méthode toString
de Document
, complétée avec la durée. La classe Video
doit être dotée en outre d’une méthode getDuree
qui retourne la durée de la vidéo représentée par l’instance courante.
Voici un squelette de la classe Video
public class Video extends Document
{
private double duree;
public Video(String n, Personne[] a, double duree)
{
// À compléter
}
public double getDuree()
{
// À compléter
}
public String toString()
{
// À compléter
}
}
Voici le constructeur de la classe Video
, qui doit appeler (en première ligne) celui de la superclasse (c’est-à-dire Document
).
public Video(String n, Personne[] a, double duree)
{
super(n, a);
this.duree = duree;
}
Voici le reste de la classe Video
, avec pour particularité l’appel de de la méthode toString
de la superclasse par celle de la classe Video
.
public double getDuree()
{
return this.duree;
}
public String toString()
{
return super.toString() + " durée : " + this.duree;
}
C-ass-01
Réalisez une classe Roman
qui représente un cas particulier de Livre
. Les instances de Roman
représentent des romans qui, en plus de tous les attributs d’un livre, ont un genre, représenté par une chaîne de caractère. Ce genre doit être l’un des paramètres du constructeur de la classe Roman
. La classe Roman
doit redéfinir la méthode toString
de sorte que cette méthode retourne la description retournée par la méthode toString
de Livre
, complétée avec le genre.
Voici un squelette de la classe Roman
public class Roman extends Livre
{
private String genre;
public Roman(String nom, Personne[] aut, long isbn, int apub, String genre)
{
// À compléter
}
public String toString()
{
// À compléter
}
}
Voici le constructeur de la classe Roman
, qui doit appeler (en première ligne) celui de la superclasse (c’est-à-dire Livre
).
public Roman(String nom, Personne[] aut, long isbn, int apub, String genre)
{
super(nom, aut, isbn, apub);
this.genre = genre;
}
Voici la méthode toString
de la classe Roman
, qui appelle la méthode toString
de sa superclasse Livre
.
public String toString()
{
return super.toString() + " genre : " + this.genre;
}
C-ass-02
Exercice créé par Marie-Laure Mugnier, Université de Montpellier
Les Pokémons sont des gentils animaux qui sont passionnés par la programmation objet en général et par le polymorphisme en particulier. Il existe quatre grandes catégories de Pokémons :
– Les Pokémons sportifs : Ces Pokémons sont caractérisés par un nom, un poids (en kg), un nombre de pattes, une taille (en mètres) et une fréquence cardiaque mesurée en nombre de pulsations à la minute. Ces Pokémons se déplacent sur la terre à une certaine vitesse que l’on peut calculer grâce à la formule suivante : vitesse = nombre de pattes * taille * 3.
– Les Pokémons casaniers : Ces Pokémons sont caractérisés par un nom, un poids (en kg), un nombre de pattes, une taille (en mètres) et le nombre d’heures par jour où ils regardent la télévision. Ces Pokémons se déplacent également sur la terre à une certaine vitesse que l’on peut calculer grâce à la formule suivante : vitesse = nombre de pattes * taille * 3.
– Les Pokémons des mers : Ces Pokémons sont caractérisés par un nom, un poids (en kg) et un nombre de nageoires. Ces Pokémons ne se déplacent que dans la mer à une vitesse que l’on peut calculer grâce à la formule suivante : vitesse = poids / 25 * nombre de nageoires.
– Les Pokémons de croisière : Ces Pokémons sont caractérisés par un nom, un poids (en kg) et un nombre de nageoires. Ces Pokémons ne se déplacent que dans la mer à une vitesse que l’on peut calculer grâce à la formule suivante : vitesse =(poids / 25 * nombre de nageoires) / 2.
Pour chacune de ces quatre catégories de Pokémons, on désire disposer d’une méthode toString qui retourne (dans une chaine de caractères) les caractéristiques du Pokémon.
Programmez ces classes en Java. Les attributs sont privés. Pour éviter de devoir créer autant de fichiers que de classes, vous pouvez ne déclarer qu’une classe "public", celle qui contient la méthode main de l’application.
Le but est de minimiser les duplications de code en utilisant le polymorphisme (méthodes abstraites, redéfinition, héritage, liaison dynamique).
Voici un exemple de méthode main permettant de tester votre solution.
public static void main(String[] args)
{
Pokemon p1 = new PokSportif("Tim", 1.2, 4, 0.3, 70);
System.out.println(p1.toString());
Pokemon p2 = new PokMaison("Tom", 1.8, 4, 0.5, 5);
System.out.println(p2.toString());
Pokemon p3 = new PokMer("Tum", 2.4, 2);
System.out.println(p3.toString());
Pokemon p4 = new PokMer("Tem", 3.3, 2);
System.out.println(p4.toString());
}
L’affichage attendu lors de l’exécution de la méthode de test est le suivant.
Tim : Pokemon sportif de fréquence cardiaque 70
4 pattes, taille = 0.3 m
vitesse : 3.59, poids : 1.2
Tom : Pokemon domestique regardant la télé 5 heures par jour
4 pattes, taille = 0.5 m
vitesse : 6.0, poids : 1.8
Tum : Pokemon de mer
2 nageoires
vitesse : 0.19, poids : 2.4
Tem : Pokemon de mer
2 nageoires
vitesse : 0.26, poids : 3.3
On peut utiliser l’architecture suivante avec 3 classes abstraites et 4 concrètes.
Commençons par la classe abstraite Pokemon
au sommet de notre hiérarchie.
abstract class Pokemon
{
String nom;
double poids;
public Pokemon(String nom, double poids)
{
this.nom = nom;
this.poids = poids;
}
public abstract double vitesse();
public abstract String description();
public String toString()
{
return nom + " : " + description() + "\nvitesse : "
+ vitesse() + ", poids : " + poids;
}
}
On met dans cette classe tout ce qui est commun à tous les Pokémons. Les attributs ne sont pas privés mais ont un accès par défaut (package) pour éviter d’alourdir le code avec des accesseurs. Le constructeur sert uniquement à être appelé par les constructeurs des classes dérivées (on ne peut pas créer une instance de classe abstraite). La méthode concrète toString
construit une description de n’importe quel Pokémon en faisant appel au méthodes abstraites vitesse et description qui seront définies dans les classes dérivées.
Il vous reste à implémenter les 2 autres classes abstraites et les 4 classes concrètes dans le même esprit.
Intéressons-nous à la classe abstraite PokTer
abstract static class PokTer extends Pokemon
{
private int pattes;
private double taille;
public PokTer(String nom, double poids, int nb_pattes, double taille)
{
super(nom, poids);
this.pattes = nb_pattes;
this.taille = taille;
}
public double vitesse()
{
return pattes * taille * 3;
}
public String description()
{
return pattes + " pattes, taille = " + taille + " m";
}
}
Cette classe regroupe tout ce qui est commun aux Pokémons sportifs et aux Pokémons casaniers un nombre de pattes, une taille, le calcul de la vitesse qui est réalisé par une méthode concrète dont les classes dérivées hériteront, et une partie de la description qui sera réutilisée dans les classes dérivées. Je vous conseille de développer maintenant la classe concrète PokSportif
et de faire des essais sur machine sans attendre d’avoir complètement terminé l’exercice.
Voici le détail de la classe PokSportif
.
static class PokSportif extends PokTer
{
private int frequence;
public PokSportif(String nom, double poids, int nb_pattes, double taille, int frequ)
{
super(nom, poids, nb_pattes, taille);
this.frequence = frequ;
}
public String description()
{
return "Pokemon sportif de fréquence cardiaque " + frequence + "\n"
\+ super.description();
}
}
On voit apparaitre l’attribut fréquence qui est spécifique aux Pokémons sportifs. Le point important est que la méthode description redéfinie ici est celle qui sera effectivement exécutée, mais qu’elle appelle la méthode description de la superclasse PokTer
qui crée la partie de la description commune aux Pokémons sportifs et aux Pokémons de maisons (casaniers).
Vous avez maintenant tous les éléments techniques pour terminer l’exercice, d’abord en implémentant l’autre type de Pokémon terrestre, puis la branche des Pokémons aquatiques.
Voici le détail de la classe PokMaison
.
static class PokMaison extends PokTer
{
private int htv;
public PokMaison(String nom, double poids, int nb_pattes, double taille, int htv)
{
super(nom, poids, nb_pattes, taille);
this.htv = htv;
}
public String description()
{
return "Pokemon domestique regardant la télé " + htv +
" heures par jour\n" + super.description();
}
}
Et la classe abstraite qui mutualise les éléments communs des Pokémons aquatiques.
static class PokAqua extends Pokemon
{
private int nageoires;
public PokAqua(String nom, double poids, int nageoires)
{
super(nom, poids);
this.nageoires = nageoires;
}
public double vitesse()
{
return *round*((poids / 25) * nageoires);
}
public String description()
{
return nageoires + " nageoires";
}
}
On retrouve ici les principales techniques mises en œuvre précédemment. On peut d’ailleurs imaginer d’autres variantes des classes déjà écrites qui seraient tout à fait acceptables. Le point intéressant est que la vitesse des Pokémons des mers et celle des Pokémons de croisière ses calcules de manière assez similaire. Une grosse partie du calcul est réalisé par la méthode vitesse de la classe abstraite PokAqua
qui sera directement héritée par la classe PokMer
et sera redéfinie dans PokCrois
. Les deux classes manquantes sont données au prochain épisode.
Voici le détail de la classe PokMer
.
static class PokMer extends PokAqua
{
public PokMer(String nom, double poids, int nageoires)
{
super(nom, poids, nageoires);
}
public String description()
{
return "Pokemon de mer\n » + super.description();
}
}
static class PokCrois extends PokAqua
{
public PokCrois(String nom, double poids, int nageoires)
{
super(nom, poids, nageoires);
}
public double vitesse()
{
return super.vitesse() / 2.0;
}
public String description()
{
return "Pokemon de mer\n" + super.description();
}
}
Ceci termine l’exercice. Il est important que vous compreniez bien ce qui se passe lorsque la méthode toString
est invoquée avec un objet de type PokCrois
. Cette méthode est définie dans la classe Pokemon
au sommet de la hiérarchie. Elle fait appel :
-
à la méthode
vitesse
de la classePokCrois
qui fait appel à la méthodevitesse
de la classePokAqua
, -
à la méthode
description
de la classePokCrois
, qui elle-même appelle la méthodedescription
dePokAqua
.
Cet enchaînement d’appels n’est pas si évident à suivre ! Il faut prendre le temps d’y réfléchir. Le principe général est qu’en cas de redéfinitions, c’est la variante de la méthode la plus « proche » de l’objet concerné qui est appelée.
Exercices de consolidation
C-cons-00
Vous devez développer une petite hiérarchie de classes permettant de représenter des ensembles d’entiers de différentes manières.
L’interface IntSet
déclare trois méthodes permettant respectivement d’ajouter un élément dans l’ensemble courant, de tester si un entier appartient l’ensemble courant, et de tester si l’ensemble courant est inclus dans un autre ensemble désigné en paramètre. Cette interface dérive de Iterator<Integer>
pour que toutes les classes implémentant cette interface dispose d’itérateurs permettant d’énumérer leurs éléments.
La classe abstraite AintSet
permet l’implémentation de la méthode subsetOf
qui utilise les méthodes abstraites isIn
et iterator
.
Quant aux deux classes BoolIntSet
et ListIntSet
, elles implémentent deux représentations différentes d’un ensemble d’entiers : une représentation par un ArrayList
d’entiers et une représentation par tableau de Booléens.
Commençons par déclarer l’interface IntSet
.
public interface IntSet extends Iterable<Integer>
{
public void add(int e);
public boolean isIn(int e);
public boolean subsetOf(IntSet s);
}
Intéressons-nous maintenant à la classe abstraite AintSet
.
public abstract class AintSet implements IntSet
{
public boolean subsetOf(IntSet s)
{
Iterator<Integer> iter = iterator();
while(iter.hasNext())
{
int e = iter.next();
if(!s.isIn(e))
{
return false;
}
}
return true;
}
}
Il faut bien comprendre que les méthodes abstraites déclarées dans l’interface IntSet
et celles héritées par cette interface sont héritées par AintSet
et sont donc implicitement présentes dans cette classe. C’est ce qui permet de mettre ici l’implémentation concrète de la méthode subsetOf
dont le code est commun aux deux représentations possibles des ensembles. Cette méthode utilise un itérateur, dont la disponibilité est garantie par le fait que AintSet
implémente indirectement (via IntSet
), l’interface Iterable<Integer>
. L’algorithme implémenté dans la méthode subsetOf
consiste à parcourir, grâce à un itérateur, tous les éléments de l’ensemble courant et de vérifier grâce à un appel à isIn
si chacun d’eux appartient à l’ensemble désigné par le paramètre s.
La méthode isIn
est abstraite, mais ce sera une de ses implémentations concrètes dans une classe dérivée qui sera exécutée en pratique. C’est toute la force du concept de liaison dynamique. Il vous reste maintenant à implémenter les deux classes dérivées BoolIntSet
et ListIntSet
. Commencez par ListIntSet
, qui sera traitée par le prochain indice. Ne le consultez pas tout de suite !
public class ListIntSet extends AintSet
{
private ArrayList<Integer> data;
class IterIntSet implements Iterator<Integer>
{
private int cursor;
public IterIntSet(){this.cursor = 0;}
public boolean hasNext()
{
if(cursor<data.size()) return true;
else return false;
}
public Integer next()
{
this.cursor++;
return data.get(cursor-1);
}
}
// À compléter
}
La classe ListIntSet
implémente concrètement un ensemble sous la forme d’une liste d’entiers. Elle doit également implémenter une classe interne de type Iterator<Integer>
. Voici une partie du code de cette classe.
Les éléments de l’ensemble représenté seront stockés dans la liste désignée par l’attribut data
. Cet attribut est accessible depuis la classe interne IterIntSet
qui implémente l’itérateur.
Cette classe IterIntSet
a son propre attribut cursor
, de type int
, qui représente une sorte de curseur permettant de parcourir la liste des éléments de l’ensemble.
Vous pouvez comprendre les détails du fonctionnement en examinant attentivement le code des différentes méthodes.
Il y a toutefois une autre approche possible, plus « professionnelle », qui consiste à utiliser un itérateur de la classe ArrayList<Integer>
, qui elle-même implémente Iterable<Integer>
. Vous pouvez essayer et tester votre solution sur machine.
Il vous reste à compléter cette classe avec les définitions du constructeur, des méthodes add
, isIn
et iterator
.
Voici le constructeur et les méthodes add
et iterator
de la classe ListIntSet
.
public ListIntSet() {this.data = new ArrayList<>();}
public void add(int e) {this.data.add(e);}
public Iterator<Integer> iterator(){return new IterIntSet();}
Et la méthode isIn
. Elle pourrait utiliser l’itérateur. Le choix qui a été fait ici est de parcourir la liste des éléments avec une boucle for
.
public boolean isIn(int e)
{
for(int x : this.data)
{
if( x== e) return true;
}
return false;
}
Il vous reste à implémenter la classe BoolIntSet
.
Voici le détail de la classe BoolIntSet
qui termine l’exercice. Prenez le temps de bien analyser son contenu.
public class BoolIntSet extends AintSet
{
private boolean[] data;
// Implémenter l'itérateur comme classe membre lui permet
// l'accès à l'attribut privé data
class IterIntSet implements Iterator<Integer>
{
private int cursor;
public IterIntSet() {this.cursor = -1;}
public boolean hasNext()
{
cursor++;
while(cursor<data.length)
{
if(data[cursor]) return true;
else cursor++;
}
return false;
}
public Integer next() {return cursor;}
}
public BoolIntSet() {this.data = new boolean[256];}
public void add(int e) {this.data[e] = true;}
public boolean isIn(int e){return this.data[e];}
public Iterator<Integer> iterator(){return new IterIntSet();}
}
public BoolIntSet() { this.data = new boolean[256]; }
public void add(int e) { this.data[e] = true; }
public boolean isIn(int e) { return this.data[e]; }
public Iterator<Integer> iterator() { return new IterIntSet(); } }
Cet exercice est difficile à maîtriser car il comporte des subtilités algorithmiques et des notions assez avancées de programmation objet.