Document ID: /fineweb-2-swissfilter-quality_10-filterrobots/filtered/07005.jsonl.gz/1024

Capsule : Héritage et sous‑typage
- Héritage et classe abstraite
- Interface
- Méthode virtuelle et redéfinition (overriding)
- Sous‑type et classe dérivée
Héritage et classe abstraite
En programmation orientée objet, la notion d’héritage recouvre le plus souvent deux aspects distincts :
- La réutilisation de code (héritage d’implémentation)
- Le sous‑typage
L’héritage d’implémentation permet de créer une nouvelle classe en étendant les fonctionnalités d’une classe existante. La classe qui est étendue est appelée classe de base (base class, parent class ou super class), et la classe qui l’étend est appelée classe dérivée (derived class ou extended class). La classe dérivée hérite de l’implémentation de la classe de base. L’héritage permet donc d’étendre une classe existante sans avoir à modifier son code. De fait, l’héritage est l’un des mécanismes qui permettent de satisfaire le principe ouvert-fermé (Open-closed principle). En Java, une classe est toujours directement ou indirectement une classe dérivée de la classe
Object. Si l’on n’utilise pas explicitement le mot clé
extends dans la déclaration d’une classe, celle‑ci étend implicitement la classe
Object, c’est pourquoi toutes les classes Java ont, notamment, des méthodes telles que
toString ou
equals qu’il est possible de redéfinir (override).
En programmation orienté objet, une classe est l’implémentation d’un type, mais cette implémentation peut être partielle. C’est-à-dire que certaines de ses méthodes d’instance publiques sont déclarées, mais pas implémentées (le corps de ces méthodes est absent). Une classe dont l’implémentation du type est partielle est une classe abstraite. Puisque l’implémentation est incomplète, cette classe ne peut pas être instanciée. Pour créer un objet dont le type est celui d’une classe abstraite, on doit étendre cette classe abstraite à l’aide du mécanisme d’héritage pour compléter l’implémentation du type. Lorsqu’une classe étend une autre classe, le type de la classe dérivée devient un sous‑type de celui de classe de base. Si l’on respecte le principe de substitution de Liskov (Liskov substitution principle), cela signifie qu’une instance de la classe dérivée peut être utilisée partout où une valeur du type de la classe de base est attendue sans modifier les propriétés du programme.
Nous pouvons étendre la définition de la notion de classe pour couvrir ce cas de figure; une classe est :
- L’implémentation complète ou partielle d’un type.
- Un truc pour construire des objets si l’implémentation est complète.
- Une unité d’organisation du code.
En UML, on représente l’extension d’une classe de base par un trait continu terminé par une pointe de flèche fermée. Dans l’exemple ci‑dessous, les classes
R et
S étendent la classe de base
T. La relation entre les classes dérivées et la classe de base est appelée relation d’extension. Le nom des classes abstraite et des méthodes virtuelles pures (voir plus bas) est écrit en italique.
Interface
De manière générale, l’interface d’une classe est l’ensemble des méthodes d’instance publiques de cette classe. En Java, une interface désigne également une forme particulière de classe que l’on déclare à l’aide du mot clé interface et qui contient seulement la signature de ses méthodes, pas leur implémentation. On peut considérer une interface comme une classe dont toutes les méthodes sont virtuelles pures. On peut donc dire qu’une interface en Java est une classe qui se réduit à la spécification d’un type (ce qui est conforme à l’acception d’interface en programmation orientée objet). Le nom d’une interface, comme le nom d’une classe, peut se trouver partout où le nom d’un type est attendu. En revanche, comme elle ne fournit pas d’implémentation pour ses méthodes, on ne peut pas l’utiliser pour créer des objets. Pour pouvoir instancier des objets du type défini par une interface, on doit d’abord créer une classe qui implémente (réalise) cette interface à l’aide du mot clé
implements. Une classe peut implémenter une ou plusieurs interfaces.
En UML, on représente l’implémentation d’une interface par une classe, à l’aide d’un trait discontinu terminé par une pointe de flèche fermée. La figure 2 montre une classe S qui implémente une interface T. La relation entre la classe qui implémente l’interface, et l’interface est appelée relation de réalisation. Dans l’exemple de la figure 3, la classe
S réalise (implémente) les interface
T et
U.
Dans l’exemple de la figure 4, l’interface
T étend les interfaces
U et
V, et la classe
R implémente l’interface
T et étend la classe
S. Il est à noter que la relation entre l’interface T et les interfaces U et V est une relation d’extension (trait continu). En effet, puisqu’une interface n’a pas d’implémentation, cela n’aurait pas de sens de dire que l’interface
T « implémente » les interfaces
U et
V. Nous disons donc que l’interface
T « étend » les interface
U et
V. En Java, la relation d’extension (trait continu) se traduit toujours par le mot clé
extends et la relation de réalisation (trait discontinu) se traduit toujours par le mot clé
implements. Cet exemple montre également que le sous-typage est transitif : si
T est un sous‑type de
U et
R est un sous‑type de
T, alors
R est un sous‑type de
T.
Méthode virtuelle et redéfinition (overriding)
Lorsqu’on crée une nouvelle classe en étendant une classe de base, on peut non seulement ajouter des méthodes, mais également redéfinir (override) les méthodes virtuelles de la classe de base. Une méthode n’est pas nécessairement virtuelle, elle peut être :
- Virtuelle pure : elle doit être redéfinie.
- Virtuelle : elle peut être redéfinie.
- Non-virtuelle : elle ne peut pas être redéfinie.
En Java, les méthodes sont virtuelles par défaut, il n’y a donc rien à faire pour permettre leur redéfinition dans une classe dérivée. On peut, par contre, interdire la redéfinition d’une méthode à l’aide du modificateur
final ou, au contraire, rendre sa redéfinition obligatoire avec le modificateur
abstract. Si l’une des méthodes d’une classe est virtuelle pure, alors cette classe doit obligatoirement être déclarée comme étant abstraite. Dans le cas d’une interface, toutes les méthodes sont implicitement virtuelles pures.
Lorsqu’on redéfinit une méthode, on peut invoquer la méthode de la classe de base à l’aide du mot clé
super. On peut, en outre, utiliser l’annotation
@Override pour indiquer au compilateur notre intention de redéfinir la méthode et lui permettre de vérifier que la signature correspond bien à la signature de la méthode originale.
Sous‑type et classe dérivée
Du point de vue du compilateur, l’utilisation d’un mécanisme d’héritage (en Java,
implements ou
extends) implique toujours la création d’un sous‑type. Cela signifie qu’il acceptera une instance d’une classe dérivée partout (à quelques exceptions près) où une instance de la classe de base est attendue.
Le compilateur ne peut pas vérifier que le sous‑type que vous avez créé répond effectivement à la définition d’un sous‑type selon le principe de substitution de Liskov; c’est au programmeur de s’assurer qu’une valeur du sous‑type qu’il a créé peut effectivement être substituée à une valeur du type de la classe de base. Pour cela, on peut utiliser la règle suivante : un type B est vraisemblablement un sous‑type d’un type A, si l’on peut dire que B est un A. Par exemple, le type «chien» peut être considéré comme un sous‑type de «mammifère», car cela a un sens de dire qu’un chien est un mammifère; a contrario, cela n’a aucun sens de dire qu’une maison est une brique, «maison» ne peut donc pas être un sous‑type de «brique». Toutefois, cette règle n’est pas absolue et même en l’appliquant, il est très facile de violer le LSP comme le montre l’exemple de la hiérarchie rectangle/carré.