
L'exemple des figures géométriques est un grand classique comme support pour enseigner l'héritage en Java ou dans d'autres langages.
Le choix des cercles et des rectangles peut paraître bizarre : manipuler des ovales et des rectangles semblerait plus logique. Deux raisons :
- cela simplifie l'écriture de la « mauvaise » solution car les constructeurs seront différents ;
- cet exemple sera repris dans un cours ultérieur sur les bonnes pratiques de conception, plus précisément sur le respect du principe de substitution de Liskov (LSP : Liskov Substitution Principle, le L de SOLID).
Cette classe est bien sûr incomplète, seuls les éléments utiles pour la suite sont donnés.
Par ailleurs, il existe bien évidemment une telle classe dans la bibliothèque Java (class Point
) qu'il serait préférable d'utiliser.
Un cercle dans un plan est caractérisé par son centre et son rayon (d'autres choix sont bien sûr possibles).
L'objectif ici n'est pas d'être exhaustif et de choisir la représentation la plus générale possible, donc nous nous contenterons d'un entier pour le rayon et notre cercle mémorisera simplement ces deux attributs.
Cela justifie le constructeur (qui devrait cependant vérifier la validité de ses arguments), nous ajoutons simplement deux méthodes afin d'illustrer par la suite l'héritage. Cette classe est donc loin d'être complète.
Nous choisissons aussi de mémoriser le centre du rectangle pour un objectif pédagogique, et
notre rectangle aura des côtés horizontaux et verticaux.
Le commentaire pour la classe Cercle
(choix de la représentation et des méthodes) s'applique bien évidemment aussi ici.
Imaginez un outil permettant de créer de telles figures (et bien d'autres bien sûr) : dans la solution actuelle (deux classes Cercle
et Rectangle
sans aucun lien), il est nécessaire de connaître le type exact de l'objet choisi par l'utilisateur pour le déplacer (c'est-à-dire pour appeler la bonne méthode move()
sur l'objet).
Si on ajoute une nouvelle sorte de figure, on doit écrire du code spécifique pour l'identifier 1 et ainsi appeler sa méthode move()
.
Une « mauvaise » solution est celle dite de « l'union » : on prévoir un attribut identifiant le type
de figure, et on décide, selon la valeur de cet attribut type
, quelle est la signification des autres attributs nécessaires pour représenter toutes les possibilités.
D'une certaine façon, on « ruse » en utilisant un même attribut pour représenter des caractéristiques différentes et exclusives de plusieurs objets (ici, l'attribut dim1
est soit le rayon du cercle, soit la largeur du rectangle).
Faire l'union de tous les attributs devient très rapidement non-maintenable.
Par exemple, tout ajout d'une nouvelle sorte de figure impose de modifier cette classe, donc de re-tester tout le code déjà écrit sans pouvoir être sûr qu'un nouveau bogue non détecté n'a pas été introduit.
De manière plus générale, une telle conception ne respecte pas un autre principe de bonne conception, le principe d'ouverture-fermeture (OCP : Open Close Principle, le O de SOLID) qui sera vu plus tard.
Pour apporter une solution satisfaisante à ce problème, nous devons revenir au concept de classe : une classe décrit la structure de ses instances et leur comportement. Quand une partie de la structure et/ou du comportement est commun à plusieurs classes, il pourrait être intéressant de factoriser cette partie commune.
La vision ensembliste (la classe est l'ensemble de ses instances) peut aussi nous aider ici : l'ensemble des cercles est inclus dans l'ensemble des figures, de même que l'ensemble des rectangles : peut-on traduire cette relation au niveau de la définition des classes ? Il faudra cependant être prudent sur cette interprétation qui peut amener à un non respect du principe de bonne conception LSP déjà évoqué.
Dans l'exemple support de ce cours, les cercles et les rectangles ont, volontairement, des caractéristiques communes : un centre de type Point
(choix de conception) qui permet d'avoir une méthode identique move()
qui déplace ce centre ; par ailleurs, il est intéressant de noter que ces deux familles d'objets peuvent répondre à un même message (appel de la méthode perimeter()
même si le code associé est différent.
L'héritage consiste à factoriser la description commune entre deux ou plusieurs classes dans une unique classe (ce qui permet donc d'éviter la duplication de cette description) nommée super-classe ; les classes qui reprennent cette description déjà faite et l'enrichissent sont dites sous-classes.
Une terminologie alternative est de parler de classe-mère (super-classe) et de classe-fille (sous-classe).
Une autre terminologie, issue de C++, utilise les termes de classe de base (super-classe) et de classe dérivée (sous-classe).
Ici, comme déjà indiqué, les caractéristiques identiques entre Cercle
et Rectangle
sont l'attribut center
et la méthode move()
.
La vision ensembliste a déjà été évoquée, cette relation entre classes vues comme l'ensemble de ses instances est souvent nommée est-un•e (is-a en anglais), qui s'oppose par exemple à a-un•e (has-a en anglais). Il faut cependant bien faire attention de raisonner au niveau des instances : ce cercle de centre p1
et de rayon 10
est-un cercle et est-une figure de centre p1
: la première affirmation correspond à l'instanciation, la seconde à l'héritage.
La super-classe est définie, au moins pour l'instant, comme une classe normale, avec ses attributs et ses méthodes.
Puisque l'attribut center
est décrit par cette classe Figure
, il est logique de prévoir un constructeur chargé d'initialiser cet attribut.
extends
est le mot clef Java pour indiquer, lors de la définition d'une classe, sa super-classe.
Quand on construit une instance de Circle
, on construit sa partie Figure
; or, Figure
a (au moins) un constructeur, donc son utilisation est obligatoire. De plus, center
est privé et ne peut être accédé en dehors de Figure
.
Il faut donc un mécanisme permettant d'appeler le constructeur d'une super-classe quand un objet d'une sous-classe est créé (et donc que le constructeur de cette sous-classe est exécuté).
Le mot clef super
permet cet appel, avec passage d'argument, du constructeur de la super-classe Figure
; cette instruction doit être la première du constructeur de Cercle
.
De manière générale, quand un objet est construit, c'est déjà le constructeur de la racine de l'arbre d'héritage qui est exécuté, puis les constructeurs des classes intermédiaires dans l'ordre jusqu'à arriver au constructeur de la classe courante.
Il est important de comprendre que cet appel du constructeur de la super-classe ne peut pas être esquivé :
- si, sur cet exemple, la ligne avec
super
est absente, le compilateur signalera une erreur ; - si un constructeur sans argument (appelé constructeur par défaut) existait dans la classe
Figure
, alors l'absence de la ligne avecsuper
ne serait pas signalée comme une erreur, mais le constructeur deCircle
appellerai quand même le constructeur deFigure
(celui par défaut).
Il y a une certaine similitude d'utilisation entre les mots-clefs this
et super
:
this
fait référence à l'objet récepteur dans le contexte de sa classe.super
fait référence à l'objet récepteur dans le contexte de sa superclasse.
Il existe de nombreuses caractérisations de l'héritage, les deux les plus évidentes sont exposées ici.
L'héritage de comportement doit rester le principal critère permettant de décider si l'utilisation de la spécialisation est un choix justifié. Le seul critère de l'héritage de structure n'est en général pas suffisant, car les inconvénients de l'héritage (couplage très fort entre deux classes, donc difficultés de maintenance et d'évolution) ne doivent pas être minimisés.
Un centre est une des caractéristiques d'un cercle, il parait dès lors raisonnable que les méthodes de Circle
puissent accéder à cet attribut si nécessaire. Ce n'est pas possible actuellement, puisque cet attribut est privé pour Figure
, donc inaccessible en dehors des méthodes de Figure
.
- L'assouplissement des règles du niveau
private
(en permettant que l'attribut soit aussi accessible aux sous-classes,Circle
ici) n'est pas une bonne réponse car il augmente le couplage et retire un des avantages importants de ce niveau de protection (il est assez facile de modifier tout ce qui estprivate
car personne, en dehors de la classe, n'en dépend). - Un accesseur (
getCenter()
dans la classeFigure
) serait parfaitement justifié ici puisque cet attribut est une caractéristique visible et connue des instances deFigure
; mais cet argument n'est pas toujours applicable. - La solution exposée ici, reprise de C++, est de prévoir un niveau de protection intermédiaire, avec le mot-clef
protected
: les attributs et méthodes de ce niveau sont accessibles aux sous-classes.
La règle de base est d'appliquer une visibilité private
(ou protected
si les sous-classes ont besoin d'y accéder) aux attributs ; tout autre choix doit être fortement argumenté.
Il est par ailleurs tentant de proposer systématiquement des accesseurs en lecture (des « getters ») ou en écriture (des « setters ») pour un attribut : ce n'est en général pas une bonne idée (cela nuit à la modularité) et ne doit donc être fait que si le besoin est manifeste (c'est encore plus vrai pour les accesseurs en écriture). Par ailleurs, un « getter » écrit sans précaution peut permettre un accès en modification sur la représentation de l'objet, ce point sera revu par la suite.
Une instance de Circle
est une instance de Figure
: il est donc possible d'appliquer sur une instance de Circle
une méthode (ici, la méthode move()
) définie dans la classe Figure
; de même, il est permis de référencer une instance de Circle
via une variable de type référence sur Figure
(f
). L'inverse (référencer une instance de Figure
via une variable de type référence sur Circle
, c'est à dire écrire c = f;
) n'est pas directement autorisé car une instance de Figure
n'est pas toujours une instance de Circle
; nous revenons sur ce point par la suite.
chooseFigure()
est supposé retourner une instance de Circle
ou une instance de Rectangle
selon le choix de l'utilisateur. Java étant typé statiquement (vérification des types à la compilation, et donc ici des méthodes appelées), le compilateur refuse l'appel d'une méthode (ici, la méthode perimeter()
) qui n'est pas déclarée dans la classe de l'objet récepteur. D'autres langages, à typage dynamique comme Python, vont chercher à l'exécution si une telle méthode existe et ne signaleront le cas échéant cet échec qu'à ce moment.
Nous savons (choix de conception) que l'utilisateur ne peut créer que des Circle
ou des Rectangle
, et donc que, quelque soit le type de l'objet créé, la méthode perimeter()
sera disponible. Cependant, pour pouvoir l'appeler sur notre objet créé, nous devons indiquer qu'il existe une méthode perimeter()
dans toutes les Figure
même si nous ne savons pas répondre à cette demande dans la classe Figure
.
Grace à cet ajout, l'appel f.perimeter()
du transparent précédent sera accepté par le compilateur Java, et de plus donnera à l'exécution le résultat escompté : si f
est une instance de Circle
, ce sera la méthode perimeter()
de Circle
qui sera exécutée, symétriquement si f
est une instance de Rectangle
.
La méthode perimeter()
qui sera exécutée est celle de la classe réelle de l'objet à l'exécution (Circle
ou Rectangle
), et non celle de la variable à la compilation (Figure
).
On parle de liaison dynamique (choix selon le type à l'exécution) par opposition à liaison statique (choix selon le type à la compilation).
On utilise aussi le terme polymorphisme (formé à partir du grec ancien πολλοί (polloí) qui signifie « plusieurs » et μορφος (morphos) qui signifie « forme ») car une même forme (l'instruction f.perimeter()
sur un objet Figure
) correspond à plusieurs comportements.
Nous avons décidé que seuls des cercles ou des rectangles pourront être créés par les utilisateurs, pas des figures car une figure est un concept, certes avec des caractéristiques, mais qui ne décrit pas complètement un objet réel, contrairement à cercle ou rectangle. Concrètement, il ne doit pas être possible de créer directement des instances de Figure
: il est possible de faire figurer ce choix dans le code Java avec le mot-clef abstract
. Ainsi, toute tentative de créer une instance de Figure
(via un new Figure()
), sera détectée comme une erreur par le compilateur. On parle donc de classe abstraite.
Ce nouveau concept de classe abstraite nous permet par ailleurs de résoudre élégamment le problème de la méthode perimeter()
: celle-ci doit être définie dans la classe Figure
(typage statique) mais nous ne pouvons pas lui donner une définition satisfaisante ; dans une classe abstraite, il est aussi possible de qualifier d'abstraite une méthode, ce qui signifie qu'il n'est pas nécessaire de lui donner une définition dans cette classe ; par contre, les sous-classes devront définir leur propre version de cette méthode si elles ne sont pas abstraites.
Nous avons vu précédemment que, Circle
étant une sous-classe de Figure
, il est possible de référencer une instance de Circle
dans une variable de type référence sur Figure
(f = c
) ; ceci est possible car la sémantique de l'héritage implique que toute instance d'une sous-classe est une instance, indirecte, de la super-classe.
La conversion inverse n'est par contre pas directement accessible car une instance de Figure
n'est pas obligatoirement une instance de Circle
; mais peut l'être.
Il est possible de vérifier ce dernier point de plusieurs façons :
- l'opérateur
instanceof
retourne un booléen indiquant si son premier argument, un objet, est une instance, directe ou indirecte, de la classe indiquée comme second argument ; - la conversion explicite (le nom du type de destination entre parenthèses précède l'expression que l'on souhaite convertir) :
- soit de déroule correctement (le résultat de l'évaluation de l'expression donne une référence compatible avec le type de destination) ;
- soit provoque l'émission d'une exception
ClassCastEception
dans le cas inverse (les exceptions seront vues plus tard) ;
- une dernière solution utilise la méthode
getClass()
(voir plus loin) dont le résultat peut être comparé avec celui obtenu via la propriétéclass
appliqué au nom d'une classe.
Dans notre exemple support, Circle
et Rectangle
ont pour super-classe Figure
; pour cette dernière, aucune super-classe n'est indiquée ; cependant, Figure
a bien une super-classe (implicitement déclarée) qui est Object
, la seule classe en Java qui n'a pas de super-classe.
Cette racine de l'arbre d'héritage offre quelques méthodes (dont certaines ont déjà été rencontrées) disponibles pour tous les objets Java. En particulier, toString()
utilisé pour l'affichage possède une définition au niveau de cette classe Object
(l'adresse de l'objet en mémoire @ le nom de la classe), ainsi que
getClass()
vu juste avant (on oubliera pour l'instant le <?>
utilisé dans le type de retour).
Quelques autres méthodes sont présentées ici (d'autres seront vues plus tard).
Nous avons déjà évoqué que ==
permet de savoir si c'est le même objet qui est accédé par deux variables différentes (identité), mais pas de comparer deux objets au sens de l'égalité.
C'est justement la méthode equals()
qui est destinée à mettre en œuvre cette notion d'égalité. Cependant, la machine virtuelle Java ne peut pas définir cette notion d'égalité pour toute les classes : il se peut que la valeur de certains attributs ne doive pas être prise en compte pour la comparaison, et par ailleurs, quand un attribut est une référence vers un autre objet, faut-il comparer cet attribut avec la sémantique d'identité ou celle d'égalité ?
La définition de equals()
au niveau de la classe Object
utilise ==
pour la comparaison. Donc, par défaut, l'égalité se réduit à l'identité.
Afin de doter notre classe Complex
d'une sémantique d'égalité satisfaisante, nous pouvons y redéfinir equals()
.
La définition de equals()
doit être en général conforme à celle présentée ici ; d'ailleurs, les IDE comme Eclipse peuvent la générer automatiquement.

La classe String
possède bien sûr une sémantique d'égalité cohérente. À noter cependant que le compilateur Java est capable de regrouper les litéraux chaîne de caractères identiques.

En Java (et contrairement à d'autres langages comme C++), une classe ne peut avoir qu'une seule super-classe : on parle d'héritage simple par opposition à l'héritage multiple.
En contre-partie, Java propose le concept d'interface que l'on peut décrire comme une classe abstraite qui n'a que des méthodes abstraites (et donc pas d'attribut). Cette restriction élimine les difficultés liées à l'héritage multiple, une classe peut donc réaliser une ou plusieurs interfaces en plus d'hériter d'une super-classe. De plus, l'héritage entre interfaces est supporté.
Une classe qui réalise une interface doit fournir une définition pour toutes les méthodes de l'interface.
Lien vers la documentation de cette interface

Lien vers la documentation de cette interface

Pour revenir à un problème évoqué plus haut, soit à définir un getter permettant d'accéder au centre d'une figure.
La solution simple retournant directement l'attribut est non satisfaisante : le point retourné peut être modifié et ainsi modifier la position de la figure !
Cette version répond à la problématique précédente, mais doit être écrite spécifiquement pour chaque classe et doit être maintenue en cohérence avec la classe.
Cette version générique ne souffre pas des précédents défauts, elle utilise le concept de clonage qui est un service rendu par un objet qui retourne une copie de lui-même.
La méthode clone()
est définie au niveau de la classe Object
. Cependant, tous les objets ne sont pas clonables, et la version par défaut de clone()
lance une exception CloneNotSupportedException
.
Pour indiquer qu'une classe supporte le clonage, il faut préciser qu'elle réalise l'interface Cloneable
. Notez que cette interface ne définie aucune opération, c'est une interface dite de marquage reconnue en tant que telle par la machine virtuelle pour autoriser le clonage.
Par convention, il est demandé de redéfinir cette méthode clone()
dans la classe implémentant l'interface, avec une visibilité publique et appelant la méthode de la superclasse.
On note que le type de retour de notre méthode clone()
n'est pas exactement celui d'Object
: c'est autorisé, on parle de covariance sur le type de retour : la classe Point
est une sous-classe de la classe Object
, le type de retour de la méthode clone()
redéfinie dans la classe Point
est un sous-type de celui de la méthode correspondante de la classe Object
.
La méthode getClass()
définie dans la classe Object
permet d'obtenir un objet qui représente la classe de l'objet récepteur. Cet objet est d'ailleurs le même que celui obtenu en utilisant la propriété class
sur un nom de classe. Ceci permet l'introspection, c'est à dire, comme le montre ces quelques exemples, la découverte à l'exécution de différentes caractéristiques de la classe (attributs, méthodes…).
Une énumération est une classe particulière pour laquelle on donne la liste des seules instances existantes de cette classe.
Puisque l'ensemble des instances d'une énumération est fixe, celles-ci sont référencées par des constantes (attributs de classe : static
, non modifiables : final
) qui doivent donc êtres en UPPERCASE pour respecter les conventions de nommage.
Ce sont bien les références qui sont constantes, les objets référencés peuvent par contre être modifiés, même si ce cas d'usage est assez rare.
Le compilateur tient compte de ces énumérations : par exemple, dans un switch
, une avertissement sera signalé si il manque au moins l'une des valeurs de l'énumération.
Toutes les énumérations héritent de la classe Enum
(elle-même, bien sûr, sous-classe de Object
) : cette super-classe offre quelques services.
Une énumération reste une classe, il est donc possible d'y ajouter des attributs, d'y définir des méthodes en plus de celles héritées de la classe Enum
.
Un record
permet de créer des classes dont les instances sont non modifiables et offrent les méthodes utilitaires nécessaires. Ces classes héritent de Record
.

© 2024 CentraleSupélec