Substitution d'objets - Hiérarchie de classes
La hiérarchie de classes correspond à une hiérarchie de type / sous type entre classes. Elle organise les trois catégories de classe : concrète, abstraite et abstraite pure. La construction de cette hiérarchie se fait au travers du mécanisme d'héritage.
L'héritage ou lien "est-un"
Définition : Le mécanisme d'héritage inclut dans la construction d'une classe les informations de la construction/ définition d'une autre classe (existante).
Si une classe D hérite d'une classe B, la définition de la classe D inclut automatiquement le prototype des méthodes (d'instance et de classe) et la réalisation (code des méthodes, variables) de la classe B.
Toutes les modifications de la classe B se répercutent sur la classe B. La classe D est appelée "classe dérivée", "sous-classe", "classe fille" et la classe B est appelée "classe de base", "classe mère".
On distingue le mécanisme d'héritage en :
- Héritage simple si le mécanisme n'accepte qu'une seule classe de base directe. La hiérarchie correspond à un arbre avec une seule racine.
- Héritage multiple si le mécanisme accepte plusieurs classes de base directes. La hiérarchie correspond à une forêt avec plusieurs racines.
Lien "est-un"
L'héritage est un lien entre classe/type nommé dans le cours "est-un" ("is-a"). D hérite de B ou D est un B. Il y a au moins deux aspects à considérer avec le lien "est-un".
- La relation de type/sous-type : La nouvelle classe inclut le prototype (et la spécification) des méthodes de l'ensemble des classes impliquées dans la hiérarchie d'héritage. Le mécanisme d'héritage définit une relation de type/sous-type. L'intérêt est de permettre la substitution d'objets. C'est une propriété pour le client de la hiérarchie.
- Réutilisation de la réalisation : La nouvelle classe inclut la réalisation (corps des méthodes et variables) de l'ensemble des classes impliquées dans la hiérarchie des méthodes et variables) de l'ensemble des classes impliquées dans la hiérarchiser d'héritage. L'intérêt est de permettre de factoriser du code. C'est une propriété pour le développeur.
Ces deux aspects ne sont pas forcément liés. Mais, en général, le mécanisme d'héritage ne les distingue pas. Il faut les gérer en même temps ce qui peut introduire des incohérences dans la hiérarchie de classes. La hiérarchie doit plutôt mettre l'accent sur la relation de type/sous-type.
En Java
Dans la construction d'une classe, le langage Java sépare une classe avec réalisation et une classe sans réalisation. Du coup, le lien "est-un" correspond à trois mécanismes d'héritages :
- l'héritage entre une classe et des interfaces
- l'héritage entre interfaces
- l'héritage entre classes
Héritages d'interfaces
C'est un mécanisme d'héritage multiple
Héritage entre une classe et des interfaces
[public][abstract] class NomClasse implements UneInterface, UneAutreInterface{
}
Le mot-clé implements indique que la nouvelle classe correspond à une
réalisation des différentes interfaces de la liste.
Héritage entre interfaces
[public] interface NomInterface extends UneInterface, UneAutreInterface{
}
La nouvelle interface "inclut" toutes les déclarations contenues dans les
interfaces spécifiées par le mot-clé extends. Ce lien "est-un" permet à une
interface d'étendre ou de réunir des fonctionnalités d'autres interfaces.
Héritage entre classes
Le mot-clé extends définit la relation d'héritage entre deux classes. C'est un
héritage simple avec comme racine la classe java.lang.Object. Une même classe
peut avoir un héritage simple sur une construction class et un héritage
multiple sur des constructions interface.
Définition d'une classe avec réalisation
[public] [abstract | final] class NomClasse [ extends UneClasse ] [ implements UneInterface , UneAutreInterface ] {
// Membres: variables .
[final][public | protected | private] type varInstance [= expression ];
static
[final][public | protected | private] type varClasse [= expression ];
// Membres méthodes
[final][public | protected | private]
typeRetour méthodeInstance ([ liste de paramètres ])
{
// code
}
abstract [public | protected] typeRetour méthodeInstance ([ liste de paramètres ]);
static [final] [public | protected | private] typeRetour méthodeClasse ([ liste de paramètres ])
{
// code
}
// Membres constructeurs .
[public | protected | private] constructeur ([ liste de paramètres ])
{
//code
}
}
Sans préence du mot-clé extends, une classe hérite par défaut de la classe
java.lang.Object. La classe Object contient des méthodes d'instance communes
à tous les objets comme toString(), equals() ou getClass() le mot clé
final devant une classe bloque l'héritage. Cette classe n'admet pas de
sous-classe. Le mot clé abstract devant une classe indique une classe
abstraite c'est à dire non instanciable. Une classe abstraite admet forcément
des sous-classes, elle ne peut pas être déclarée avec le mot clé "final". Pour
notre cas d'école : nous souhaitons ajouter un service de verrou à
l'encapsulation porte charnière.
package porte;
public class PorteVerrouCharniere extends PorteCharniere{
private boolean estVerrouille;
public PorteVerrouCharniere(){
estVerrouille=false;
}
public void verrouiller(){
estVerrouille=true;
}
public void deverrouiller(){
estVerrouille=false;
}
public boolean estVerouille(){
return estVerrouille;
}
}
PorteVerrouCharniere verrou = new PorteVerrouCharniere();
verrou.fermer();
verrou.estFerme();
verrou.estVerrouille();
Les membres d'une classe dérivée regroupent :
- les variables, méthodes et constructeurs définis dans la classe,
- les membres hérités (variables et méthodes) définis dans les classes de base (directe ou indirecte)
Accès pour les classes dérivées
Une classe dérivée ne fait pas forcément partie du même paquetage que sa classe
de base. Le modificateur d'accès protected permet un accès préférentiel par
classes dérivées aux membres de la classe de base et aux classes du paquetage.
Pour éviter les dépendances de réalisation entre la classe de base et ses
classes dérivées, il est nécessaire de respecter l'encapsulation en évitant
d'avoir des attributs en accès protected.
Variable héritée
Les variables héritées d'une classe correspondent à l'ensemble des variables d'instance et des variables de classes définies dans la hiérarchie de classes (à partir de sa classe de base).
L'instanciation d'une classe dérivée crée une seule zone mémoire. Toutes les variables d'instance héritées (accessibles ou non) se trouvent dans cette zone mémoire.
Il n'est pas possible de modifier la déclaration (type, portée) d'une variable héritée.
Constructeur
Les constructeurs ne sont pas hérités. Il faut systématiquement les définir dans la classe dérivée.
L'instanciation d'une classe dérivé est valide uniquement si les attributs définis par les classes de base sont initialisés. Les attributs d'une classe de base sont initialisés avant ceux de la classe dérivée.
Le code du constructeurs de la classe dérivée ne peut s'exécuter qu'après celui
du constructeur de la classe de base. Cette ordre d'initialisation impose un
chaînage d'appel des constructeurs des classes de base. En général, le
programmeur déclenche explicitement l'appel à un des constructeurs de la classe
de base directe en utilisant l'instruction super(). Le choix du constructeur
se fait à partir du type des paramètres (mécanisme de surcharge). L'instruction
super() doit se trouver en première ligne du code du constructeur.
class PorteVerrouCoulissante extends PorteCoulissante{
//...
public PorteVerrouCoulissante(int pasMax){
super(pasMax);
//initialisation des attributs de la classe
}
}
Lorsqu'il n'y a pas d'instruction super() ou this() sur la première ligne du
code du constructeur, le compilateur fait appel au constructeur sans paramètre
de la classe de base ("appel implicite").
public PorteVerrouCharniere(){
estVerrouille = false;
}
Méthode héritée
En fonction des portées, les méthodes d'instance héritées sont applicables sur l'instance de la classe dérivée. Les méthodes de classe de la classe de base sont applicables sur la classe dérivée.
PorteVerrouCharniere v = new PorteVerrouCharniere();
v.fermer();
v.verrouiller();
Le mécanisme de redéfinition des méthodes d'instance
Dans une classe dérivée, il est parfois nécessaire d'adapter le code d'une méthode d'instance héritée à la nouvelle réalisation. La redéfinition permet de réécrire le code d'une méthode héritée. Ce mécanisme n'est applicable que sur une méthode accessible et en respectant le prototype déclaré dans la classe de base
- la portée peut être élargie (protégée en public, paquetage en protégé ou public)
- le type de retour peut-être un sous type du type de retour de la méthode
Prenons l'exemple de la classe PorteVerrouCharniere. Cette classe hérite de la
classe PorteCharniere et doit redéfinir le code de la méthode d'instance
ouvrir() mais en réutilisant le code de cette méthode dans la classe de base.
public class PorteVerrouCharniere extends porte.PorteCharniere{
//...
public void ouvrir(){
if(!estVerrouille())
ouvrir();
}
}
Dans ce cas, nous avons un appel récursif. Nous devons avoir un moyen de
désigner la définition de la méthode dans la classe de base directe :
super.ouvrir(). Par défaut, toutes les méthodes d'instance accessibles par la
classe dérivée peuvent être redéfinies. Le mot clé final devant le prototype
de la méthode empêche la redéfinition de cette méthode dans les classes
dérivées.