3. Héritage

3.1. Définition de l'héritage

Nous avons déjà utilisé de nombreuses classes dans ce cours, et également utilisé le mécanisme de l’héritage. Nous savons déjà plusieurs choses :
  • Une classe B qui hérite d’une classe A est une sous-classe de A, et A est la super-classe de B.
  • La classe java.lang.Object est la super-classe de toutes les classes Java, directement ou indirectement.
Pour créer une classe B qui hérite d’une classe A, il faut utiliser la syntaxe suivante.

Exemple 81. Une classe qui hérite d'une autre classe

public  class A {  // déclarée dans le fichier A.java  
    ...  
}  
  
 public  class B  extends A {  // déclarée dans le fichier B.java  
    ...  
}

On peut dire aussi que B étend A, ou dérive de A, ou est une classe dérivée de A. On peut également exprimer cette relation en termes de parent / enfant, les parents étant les super classes, et les enfants les sous-classes. On dit aussi que les objets de B sont des objets de A. Cette dernière façon d'exprimer la relation d'héritage est très pratique, et permet de bien comprendre cette notion d'héritage. Une des particularités de Java par rapport à d’autres langages objet tels que le C++, est que l’héritage multiple n’est pas autorisé. Une classe ne peut hériter que d’une seule classe à la fois, ce qui simplifie grandement les choses, notamment les problèmes de collision de noms.

3.2. Conséquences pour les membres

Toutes les membres public et protected d'une classe parent sont accessibles à ses classes enfant. En revanche, une classe parent ne peut accéder à ses classes enfant. Lorsque l'on définit une hiérarchie de classes, on cherche donc à répartir les méthodes les plus générales sur les classes parent, et les méthodes les plus spécialisées sur les classes enfant. Ce que l'on appelle "sous-classe" correspond en fait aux classes possédant le plus de méthodes. Si un champ d'une classe enfant porte le même nom qu'un champ d'une classe parent, alors le champ enfant masque le champ parent. Par défaut, le code de la classe enfant accède au champ enfant. En revanche, le code de la classe parent accède au champ de sa classe. Il est possible pour un code de la classe enfant de lire le champ de la classe parent en utilisant le mot-clé super.

Exemple 82. Accès aux champs d'une super-classe - 1

public  class A {  // déclarée dans le fichier A.java  
     protected String nom =  "Je suis dans A" ;  
      
     public  void uneMethode() {  
        System.out.println(nom) ;  // imprime "Je suis dans A"  
    }  
}  
  
 public  class B  extends A {  // déclarée dans le fichier B.java  
     protected String nom =  "Je suis dans B" ;  
  
     public  void uneAutreMethode() {  
        System.out.println(nom) ;  // imprime "Je suis dans B"  
        System.out.println(super.nom) ;  // imprime "Je suis dans A"  
    }  
}

Notons le comportement suivant, lors d'une assignation.

Exemple 83. Accès aux champs d'une super-classe - 2

public  class Main {  // déclarée dans le fichier Main.java  
  
     public  static  void main(String... args) {  
        A a =  new A() ;  // la classe A est celle que nous venons de définir  
        B b =  new B() ;  // idem  
        A ba = b ;       // cette déclaration est légale, car b est un élément de A, 
                         // du fait que B étend A ;   
                         // b et ba désignent le même objet  
        System.out.println("a.nom = " + a.nom) ;  
        System.out.println("b.nom = " + b.nom) ;  
        System.out.println("ba.nom = " + ba.nom) ;  
    }  
}

Le résultat est le suivant, et constitue un grand classique des pièges posés dans les tests d'évaluation en programmation orientée objet.
 > a.nom = Je suis dans A
 > b.nom = Je suis dans B
 > ba.nom = Je suis dans A
S'amuser à écrire des collisions de nom de la sorte est un jeu qui ne doit pas dépasser le cadre de l'écriture d'un poly. On évitera de noter les bonnes pratiques qui ont été sournoisement piétinées dans cet exemple dans le seul but de le rendre incompréhensible : utilisation de champs non privés, collisions de nom pour un champ dans une classe et une extension, etc...

3.3. Polymorphisme

La traduction des deux termes overloading et overriding mène à des choses assez amusantes, et surtout très hermétiques. Conservons donc les termes anglais ici. En toute rigueur, l' overloading n'est pas vraiment du polymorphisme, seul l' overriding l'est. En écrivant cela, je me rends compte que les polémiques entre les différents ayatollah de la programmation orientée objet sont éternelles. Evitons de rentrer dans ces passionnants débats, et expliquons juste les concepts. L' overload consiste à écrire deux méthodes de même nom dans une même classe, prenant des paramètres différents. C'est une technique très fréquemment utilisée. Quand un appel de méthode est fait, le compilateur commence par établir la liste des méthodes portant le bon nom. Ensuite, il regarde la liste des paramètres, trouve celle des méthodes qui correspond le mieux. Le problème n'est pas aussi simple qu'il y paraît, notamment du fait des mécanismes d'auto-boxing introduits dans Java 5. Cette résolution est effectuée à la compilation . L'utilisation de telle méthode plutôt que telle autre est donc décidée à la compilation , et non pas à l' exécution . L' override consiste à redéfinir une méthode d'une classe parent dans une classe enfant. C'est cela ce que l'on appelle la surcharge de méthode en français. Le problème est que ce terme est aussi une traduction d'overload... Cette fois, le compilateur place juste les informations nécessaires à la résolution de l'appel de la méthode, qui est faite à l' exécution . Bien souvent, il n'est pas possible, à la compilation, de déterminer quel objet va effectivement être appelé. Ce mécanisme s'appelle aussi late binding ou delayed binding dans la documentation en anglais. Le terme binding signifie lier en anglais, late binding signifie donc que l'appel à la méthode est lié à la méthode appelée de façon tardive, c'est à dire à l' exécution . Reprenons notre exemple précédent, en le modifiant un peu.

Exemple 84. Overloading et overriding

public  class A {  // déclarée dans le fichier A.java  
     public  void ouSuisJe() {  
        System.out.println("Je suis dans A !") ;  
    }  
}  
  
 public  class B  extends A {  // déclarée dans le fichier B.java  
     public  void ouSuisJe() {  
        System.out.println("Je suis dans B...") ;  
    }  
}  
  
 public  class Main {  // déclarée dans le fichier Main.java  
  
     public  static  void main(String ... args) {  
        A a =  new A() ;  // mêmes déclarations que dans l'exemple précédent  
        B b =  new B() ;  
        A ba = b ;  
  
        System.out.println("a.ouSuisJe ? " + a.ouSuisJe()) ;  
        System.out.println("b.ouSuisJe ?" + b.ouSuisJe()) ;  
        System.out.println("ba.ouSuisJe ? " + ba.ouSuisJe()) ;  
    }  
}

Cette fois-ci nous obtenons le résultat :
 > a.ouSuisJe ? Je suis dans A !
 > b.ouSuisJe ? Je suis dans B...
 > ba.ouSuisJe ? Je suis dans B...
Dans ce cas, la méthode qui doit être appelée est déterminée à l'exécution, et même si un objet de B est déclaré en tant qu'objet de A, ce sont les méthodes de B qui sont appelées. Enfin notons que les constructeurs ne sont pas des méthodes d'une classe au sens strict, la notion de surcharge n'a donc pas de sens pour eux.

3.4. Empêcher l'héritage

Il est possible d'empêcher qu'une classe soit surchargée par une autre classe, ou qu'une méthode soit surchargée par une méthode d'une sous-classe. Il suffit pour cela d'utiliser le mot-clé final, pris en compte à la compilation.

Exemple 85. Utilisation du mot-clé final

public  final  class A {  // déclarée dans le fichier A.java, ne peut être étendue  
     public  void ouSuisJe() {  
        System.out.println("Je suis dans A !") ;  
    }  
}  
  
 public B {  // déclarée dans B.java  
     public  final  void ouSuisJe() {  // ne peut être surchargée, bien que 
                                    // B puisse être étendue  
        System.out.println("J'y suis j'y reste !") ;  
    }  
}

De nombreuses classes de l'API standard de Java sont finales : c'est le cas notamment de String et de toutes les classes enveloppes des types de base. De même, les méthodes wait() de la classe Object sont finales, on ne peut donc pas les étendre. Empêcher d'étendre une classe ou une méthode est une mesure de sécurité. Cela garantit effectivement que le comportement des objets que l'on manipule est bien celui que l'on attend. Par exemple, la classe String est immutable, ce qui signifie qu'une fois instanciée, on ne peut changer la valeur de la chaîne de caractères portée par l'objet. Si elle n'était pas finale, il serait possible de l'étendre en changeant ce comportement. Ces nouveaux objets pourraient être injectés dans n'importe quel code utilisant des String, et faire échouer des processus entiers.
Java langage & API
Retour au blog Java le soir
Cours & Tutoriaux
Table des matières
Introduction : un peu d'histoire
1. Java : genèse d'un nouveau langage
Programmer en Java
1. Un premier exemple
1.1. Aperçu général, cycle de vie
1.2. Un premier programme
1.3. Programmes, applets, servlets, etc...
2. Une première classe
2.1. Écriture d'une classe
2.2. Instanciation de cette classe
3. Types de base, classes et objets
3.1. Types de base
3.2. Classes et objets
Classes Object et String
1. Introduction
2. La classe Object
2.1. La méthode toString()
2.2. La méthode clone()
2.3. La méthode equals()
2.4. La méthode hashCode()
2.5. La méthode finalize()
2.6. La méthode getClass()
3. La classe String
3.1. Introduction
3.2. Construction d'un objet de type String
3.3. Concaténation, StringBuffer et StringBuilder
3.4. Concaténations de chaînes de caractères depuis Java 8
3.5. Concaténations de chaînes de caractères depuis Java 11
3.6. Extraction d'une sous-chaîne de caractères
3.7. Comparaison de deux chaînes de caractères
3.8. Méthodes de comparaisons lexicographiques
3.9. Méthode de recherche de caractères
3.10. Méthode de modification de chaîne
3.11. Méthode de duplication
3.12. Support de l'unicode, internationalisation
Structure d'une classe
1. Introduction
2. Classes
2.1. Classes publiques
2.2. Classes internes
2.3. Classe membre
2.4. Classes locales
2.5. Classes anonymes
2.6. Le mot-clé this
3. Éléments statiques
3.1. Champ statique
3.2. Cas des constantes
3.3. Bloc statique
3.4. Classe membre statique
4. Membres d'une classe, visibilité
4.1. Bloc non statique
4.2. Accès à un membre, visibilité
4.3. Les champs
4.4. Signature d'une méthode
4.5. Les méthodes
4.6. Getters et Setters
5. Constructeur, instanciation
5.1. Chargement d'une classe
5.2. Constructeurs d'une classe
5.3. Instanciation d'un objet
5.4. Destruction d'objets
5.5. Le mot-clé final
6. Énumérations
6.1. Déclaration d'une énumération
6.2. Classe énumération
6.3. Méthode toString()
6.4. Méthode valueOf()
6.5. Méthode values()
6.6. Méthode ordinal()
6.7. Méthode compareTo()
6.8. Constructeurs privés
6.9. Classe utilitaire : EnumSet
Noms, opérateurs, tableaux
1. Introduction
2. Identificateurs, noms et expressions
2.1. Identificateurs et noms
2.2. Expressions
3. Opérateurs, ordre d'exécution
3.1. Ordre d'exécution
3.2. Les opérateurs
3.3. Les opérateurs ++ et --
3.4. Les opérateurs % et /
3.5. Les opérateurs <<, >> et >>>
3.6. L'opérateur instanceof
3.7. Les opérateurs &, | et ^
3.8. Les opérateurs && et ||
3.9. L'opérateur ? ... :
3.10. Les opérateurs d'affectation
4. Tableaux
4.1. Création d'un tableau
4.2. Initialisation d'un tableau
4.3. Utilisation d'un tableau comme un Object
4.4. Tableaux de tableaux
4.5. Copie de tableaux
5. Blocs, boucles et contrôles
5.1. Blocs
5.2. Mots-clés réservés
5.3. Tests : if et switch
5.4. Boucles : for, while, do ... while
5.5. Commandes continue et break
5.6. Commandes return et goto
Nombres, précision, calculs
1. Introduction
2. Calculs
2.1. Précision
2.2. Codage des nombres flottants
2.3. Le mot-clé strictfp
2.4. Conversion de types
3. Dépassements de capacité
3.1. Cas des entiers
3.2. Cas des flottants
3.3. Bibliothèques BigInteger et BigDecimal
4. Fonctions mathématiques
4.1. Fonctions usuelles
4.2. Générateurs aléatoires
5. Classes enveloppe
5.1. Associer les types de base à des objets
5.2. Auto-boxing
Héritage, abstraction, interfaces
1. Introduction
2. Abstraction et encapsulation
2.1. Abstraction
2.2. Encapsulation
3. Héritage
3.1. Définition de l'héritage
3.2. Conséquences pour les membres
3.3. Polymorphisme
3.4. Empêcher l'héritage
4. Classes abstraites
5. Interfaces
5.1. Introduction
5.2. Définition
5.3. Java 8 et les interfaces
5.4. Utilisation des interfaces
5.5. Définition de constantes dans les interfaces
5.6. Utilité des interfaces
Packages
1. Introduction
2. Notion de paquet
2.1. Déclaration d’appartenance à un paquet
2.2. Chargement d’une classe
2.3. Choix de nom
3. Archives, chemin de recherche, classpath
3.1. Archives
3.2. Variable CLASSPATH
3.3. Notion de classloader
3.4. Bilan sur les classes chargées
3.5. Visibilité
3.6. Conseils d'écriture, bibliothèque standard
Exceptions
1. Introduction
2. Erreurs et Exceptions
2.1. Classe Throwable, notion de stack trace
2.2. Classe Error
2.3. Classe RuntimException
2.4. Classe Exception
3. Déclenchement d'une exception
3.1. Exceptions déclenchées par la JVM
3.2. Exceptions déclenchées par l'application
4. Capter une exception
4.1. Traiter une exception localement
4.2. Code de captage
5. Créer ses propres exceptions
Entrées / sorties
1. Introduction
2. Notion de fichier
2.1. Introduction
2.2. La classe File
2.3. Construction d'une instance de File
2.4. Méthodes exposées
3. Flux de sortie
3.1. Introduction, notion de flux
3.2. Écriture de caractères, classe Writer
3.3. Bufferisation, construction d'un flux sur un autre
3.4. Utilisation de PrintWriter
3.5. Écriture d'octets, OuputStream
3.6. Écriture de types primitifs : DataOutputStream
3.7. Écriture d'objets : ObjectOutputStream
4. Flux d'entrée
4.1. Introduction
4.2. Lecture de caractères, classe Reader
4.3. Bufferisation, lecture ligne par ligne
4.4. Lecture d'octets : InputStream
4.5. Lecture de types primitifs : DataInputStream
4.6. Lecture d'objets : ObjectInputStream
5. Lecture et écriture de flux croisés
5.1. Introduction
5.2. Lire des caractères sur un flux binaire : classe InputStreamReader
5.3. Écrire des caractères sur un flux binaire : classe OutputStreamWriter
5.4. Remarque sur les jeux de caractères
6. Serialization d'objets
6.1. Enjeu de la sérialization d'objets
6.2. Serialization d'un objet
6.3. Sérialization d'une grappe d'objets
6.4. Première surcharge : méthode writeObject() readObject()
6.5. Deuxième surcharge : utilisation d'un externalizer
6.6. Troisième surcharge : utilisation d'un objet proxy
7. Flux compressés
7.1. Introduction
7.2. Flux de type gzip
7.3. Flux de type zip