Comprendre l’Héritage en Java

Introduction au Concept d’Héritage en Java

L’héritage est l’un des piliers fondamentaux de la programmation orientée objet (POO). Il permet la création de relations entre les classes, favorisant ainsi la réutilisation du code, la modularité et une conception logique des applications. En Java, l’héritage est un mécanisme essentiel qui permet à une classe d’hériter des attributs et des méthodes d’une autre classe, appelée superclasse ou classe de base.

Fonctionnement de l’Héritage

L’héritage en Java est basé sur une relation « est-un ». Cela signifie qu’une classe dérivée, appelée sous-classe, est une version plus spécifique de sa classe parente. Par exemple, si nous avons une classe Animal comme superclasse, nous pouvons avoir des sous-classes telles que Chien, Chat et Oiseau, qui héritent toutes des attributs et des méthodes de la classe Animal.

Exemple d’Héritage en Java

Considérons un exemple simple où nous avons une classe Animal comme superclasse et deux sous-classes Chien et Chat. La classe Animal peut avoir des attributs comme nom et des méthodes comme manger(), tandis que les sous-classes Chien et Chat peuvent avoir des attributs et des méthodes spécifiques à ces types d’animaux.

// Classe Animal (superclasse)
public class Animal {
    protected String nom;

    public Animal(String nom) {
        this.nom = nom;
    }

    public void manger() {
        System.out.println(nom + " mange de la nourriture.");
    }
}

// Sous-classe Chien
public class Chien extends Animal {
    public Chien(String nom) {
        super(nom);
    }

    public void aboyer() {
        System.out.println(nom + " aboie.");
    }
}

// Sous-classe Chat
public class Chat extends Animal {
    public Chat(String nom) {
        super(nom);
    }

    public void miauler() {
        System.out.println(nom + " miaule.");
    }
}

Dans cet exemple, la classe Chien et la classe Chat héritent de la classe Animal, ce qui leur permet d’accéder aux attributs et méthodes définis dans la classe Animal. De plus, chaque sous-classe peut avoir ses propres méthodes spécifiques, comme aboyer() pour un chien et miauler() pour un chat.

Représentation UML de l’Héritage

Dans le langage de modélisation UML (Unified Modeling Language), l’héritage est représenté à l’aide de diagrammes de classes. Un diagramme de classes UML illustre les relations et les structures entre les différentes classes dans un système logiciel.

Dans un diagramme de classes UML, l’héritage est représenté par une flèche pointant de la sous-classe vers la superclasse. La flèche est accompagnée du mot-clé « extends » pour indiquer la relation d’héritage entre les deux classes.

Voici un exemple simplifié :

+--------------+             +----------------+
|    Animal    | <--------   |     Chien      |
+--------------+             +----------------+

Dans cet exemple, la classe Chien hérite de la classe Animal. La flèche pointe de Chien vers Animal, ce qui signifie que Chien est une sous-classe de Animal. La notation extends n’est pas affichée dans le diagramme UML, mais elle est implicite dans la flèche de l’héritage.

Exemple de Diagramme de Classes UML avec Héritage

Considérons un exemple plus concret où nous avons une hiérarchie de classes pour représenter différents types de véhicules. Voici à quoi pourrait ressembler un diagramme de classes UML pour cette situation :

+------------------+           +------------------------+
|     Véhicule     | <-------- |       Voiture          |
+------------------+           +------------------------+
|                  |           |  - nombreRoues: int    |
|  - marque: String|           |  - couleur: String     |
|  - modèle: String|           |  + démarrer(): void    |
+------------------+           |  + arrêter(): void     |
                               +------------------------+
                                         ^
                                         |
                               +------------------------+
                               |       Moto             |
                               +------------------------+
                               |  - cylindrée: int      |
                               |  - annéeFabrication: int|
                               |  + accélérer(): void  |
                               |  + freiner(): void     |
                               +------------------------+

Dans ce diagramme, la classe Voiture et la classe Moto héritent toutes deux de la classe Véhicule. Cela signifie que Voiture et Moto partagent les attributs et les méthodes définis dans la classe Véhicule, comme la marque et le modèle. En outre, chaque sous-classe peut avoir ses propres attributs et méthodes spécifiques, comme nombreRoues pour Voiture et cylindrée pour Moto.

Et que vient faire la classe java.lang.Object dans l’histoire ?

La classe java.lang.Object joue un rôle central dans le cadre de l’héritage en Java. En fait, toutes les classes Java, explicites ou implicites, héritent directement ou indirectement de la classe Object. Voici ce que cela signifie et comment cela impacte la conception de votre programme :

Importance de la classe java.lang.Object

  1. Rôle Fondamental : java.lang.Object est la classe racine de la hiérarchie de classes Java. Elle est au sommet de la chaîne d’héritage de toutes les autres classes Java.
  2. Méthodes de Base : La classe Object définit plusieurs méthodes fondamentales que toutes les autres classes héritent :
    • toString(): Pour représenter l’objet sous forme de chaîne de caractères.
    • equals(Object obj): Pour comparer l’égalité de deux objets.
    • hashCode(): Pour retourner un code de hachage pour l’objet.
    • getClass(): Pour obtenir la classe de l’objet.
    • clone(): Pour créer une copie de l’objet.
    • finalize(): Pour effectuer des actions avant la destruction de l’objet par le ramasse-miettes.
  3. Héritage Implicite : Si une classe ne spécifie pas une classe parente avec le mot-clé extends, elle hérite automatiquement de Object.
  4. Polymorphisme : La classe Object est souvent utilisée lorsqu’un code doit traiter un objet de manière générique sans connaître son type réel, grâce au polymorphisme.
  5. Méthode toString(): Il est courant de redéfinir la méthode toString() dans les classes que vous créez pour fournir une représentation textuelle significative de l’objet.

Exemple d’Utilisation de Object

public class Exemple {
    public static void main(String[] args) {
        // Création d'un objet de type String
        String str = "Bonjour";
        
        // Utilisation de la méthode toString() héritée de Object
        System.out.println(str.toString()); // Affiche "Bonjour"
        
        // Comparaison de deux objets
        String autreStr = "Bonjour";
        System.out.println(str.equals(autreStr)); // Affiche true
    }
}

Dans cet exemple, la classe String hérite de Object. Ainsi, nous pouvons utiliser les méthodes définies dans Object comme toString() et equals() avec des objets de type String.

Mise en oeuvre de l’héritage

La mise en œuvre de l’héritage en Java implique la création de relations de parenté entre les classes, permettant ainsi la réutilisation du code et la spécialisation des fonctionnalités. Voici comment vous pouvez mettre en œuvre l’héritage dans vos programmes Java :

Utilisation du Mot-Clé extends

En Java, l’héritage est réalisé en utilisant le mot-clé extends. Voici un exemple simple de mise en œuvre de l’héritage :

// Superclasse (ou classe parente)
public class Animal {
    public void manger() {
        System.out.println("L'animal mange.");
    }
}

// Sous-classe (ou classe fille)
public class Chien extends Animal {
    public void aboyer() {
        System.out.println("Le chien aboie.");
    }
}

Dans cet exemple, la classe Chien hérite de la classe Animal à l’aide du mot-clé extends. Cela signifie que la classe Chien hérite de toutes les méthodes publiques et protégées de la classe Animal, y compris la méthode manger().

Enrichissement de la Classe avec des Spécificités

Une fois que vous avez hérité d’une classe, vous pouvez enrichir la sous-classe en ajoutant de nouvelles méthodes et attributs spécifiques. Par exemple :

// Ajout d'attributs et de méthodes spécifiques à la sous-classe Chien
public class Chien extends Animal {
    private String race;

    public Chien(String race) {
        this.race = race;
    }

    public void aboyer() {
        System.out.println("Le chien de race " + race + " aboie.");
    }
}

Dans cette version, la classe Chien possède un attribut race et un constructeur pour initialiser cet attribut. Elle a également une méthode spécifique aboyer() qui affiche le type de race du chien.

Définition de Constructeurs et Utilisation du Mot-Clé super

Lorsque vous définissez des constructeurs dans une sous-classe, vous pouvez utiliser le mot-clé super pour appeler le constructeur de la classe parente et initialiser ses attributs. Par exemple :

// Définition d'un constructeur dans la sous-classe Chien
public class Chien extends Animal {
    private String race;

    public Chien(String nom, String race) {
        super(nom); // Appel du constructeur de la classe parente
        this.race = race;
    }
}

Dans cet exemple, le constructeur de la classe Chien appelle d’abord le constructeur de la classe Animal en utilisant super(nom) pour initialiser le nom de l’animal, puis il initialise l’attribut race spécifique à la classe Chien.

Assistance Eclipse à la Production de Constructeurs

Assistance IntelliJ à la Production de Constructeurs

Redéfinition de Méthode en Java

En Java, pour redéfinir une méthode héritée dans une sous-classe, vous devez déclarer une méthode avec la même signature (nom et paramètres) que la méthode de la superclasse. Voici un exemple :

// Superclasse
public class Animal {
    public void faireDuBruit() {
        System.out.println("L'animal fait un bruit.");
    }
}

// Sous-classe qui redéfinit la méthode faireDuBruit()
public class Chien extends Animal {
    @Override
    public void faireDuBruit() {
        System.out.println("Le chien aboie.");
    }
}

Dans cet exemple, la classe Chien redéfinit la méthode faireDuBruit() héritée de la classe Animal. En utilisant l’annotation @Override, nous indiquons explicitement au compilateur que cette méthode est une redéfinition d’une méthode de la superclasse. Lorsque vous appelez la méthode faireDuBruit() sur un objet de type Chien, la version spécifique de la méthode dans la classe Chien est exécutée.

Utilisation de l’Annotation @Override

L’utilisation de l’annotation @Override est facultative mais fortement recommandée lors de la redéfinition de méthodes. Elle permet de détecter les erreurs de syntaxe potentielles et de garantir que vous redéfinissez correctement une méthode héritée. Si vous utilisez l’annotation @Override et qu’il y a une erreur dans votre signature de méthode (par exemple, une faute de frappe dans le nom de la méthode), le compilateur générera une erreur.

Appel d’une Méthode de la Classe Parente, Redéfinie par la Classe Fille

Lorsque vous redéfinissez une méthode dans une sous-classe, vous pouvez toujours appeler la version de la méthode de la superclasse en utilisant le mot-clé super. Voici un exemple :

// Sous-classe qui redéfinit la méthode faireDuBruit() et appelle la version de la superclasse
public class Chien extends Animal {
    @Override
    public void faireDuBruit() {
        super.faireDuBruit(); // Appel de la méthode de la superclasse
        System.out.println("Le chien aboie.");
    }
}

Dans cet exemple, la méthode faireDuBruit() de la superclasse Animal est appelée en premier à l’aide de super.faireDuBruit(), puis la sous-classe Chien ajoute son propre comportement en imprimant « Le chien aboie. ».

Le polymorphisme est un concept fondamental de la programmation orientée objet (POO) qui permet à une même méthode d’avoir des comportements différents en fonction du type de l’objet sur lequel elle est appelée. Cela permet de traiter des objets de différentes classes de manière uniforme, ce qui favorise la réutilisation du code et la flexibilité de conception. Voici comment le polymorphisme est implémenté en Java :

Définition du Polymorphisme

En Java, le polymorphisme se produit lorsque vous appelez une méthode sur un objet et que le comportement de cette méthode dépend du type de l’objet. Il existe deux formes principales de polymorphisme en Java : le polymorphisme statique (lié au temps de compilation) et le polymorphisme dynamique (lié au temps d’exécution).

  1. Polymorphisme Statique : Le polymorphisme statique, également appelé liaison tardive ou liaison dynamique, se produit lorsque la méthode à exécuter est déterminée au moment de l’exécution du programme. Cela permet à une méthode d’être appelée de manière polymorphe à condition qu’elle soit redéfinie dans les sous-classes. Cela favorise la flexibilité et l’évolutivité du code.

Dans le polymorphisme statique, la méthode à appeler est déterminée au moment de l’exécution du programme, en fonction du type réel de l’objet.

class Animal {
    void faireDuBruit() {
        System.out.println("L'animal fait un bruit.");
    }
}

class Chien extends Animal {
    void faireDuBruit() {
        System.out.println("Le chien aboie.");
    }
}

class Chat extends Animal {
    void faireDuBruit() {
        System.out.println("Le chat miaule.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal monAnimal = new Chien(); // Polymorphisme statique
        monAnimal.faireDuBruit(); // Appelle la méthode faireDuBruit() de Chien
    }
}
  1. Polymorphisme Dynamique : Le polymorphisme dynamique, également appelé liaison précoce ou liaison statique, se produit lorsque la méthode à exécuter est déterminée au moment de la compilation. Cela se produit lorsque vous appelez une méthode sur un objet et que Java sélectionne la méthode appropriée à exécuter en fonction du type déclaré de l’objet, et non du type réel de l’objet.

Dans le polymorphisme dynamique, la méthode à appeler est déterminée au moment de la compilation du programme, en fonction du type déclaré de la référence.

class Animal {
    void faireDuBruit() {
        System.out.println("L'animal fait un bruit.");
    }
}

class Chien extends Animal {
    void faireDuBruit() {
        System.out.println("Le chien aboie.");
    }
}

class Chat extends Animal {
    void faireDuBruit() {
        System.out.println("Le chat miaule.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal monAnimal = new Chien(); // Polymorphisme dynamique
        monAnimal.faireDuBruit(); // Appelle la méthode faireDuBruit() de Chien (liaison précoce)
    }
}

Dans cet exemple, la référence monAnimal est de type Animal. Même si l’objet réel est un Chien, la méthode faireDuBruit() est sélectionnée au moment de la compilation en fonction du type déclaré de la référence (Animal). C’est ce qu’on appelle la liaison précoce ou polymorphisme dynamique.

Utilisation de l’opérateur instanceof

L’opérateur instanceof est un opérateur en Java qui permet de vérifier si un objet est une instance d’une classe spécifique, ou d’une classe dérivée de cette classe. Cela permet de réaliser des opérations conditionnelles en fonction du type réel d’un objet. Voici comment utiliser l’opérateur instanceof en Java :

Syntaxe de l’Opérateur instanceof

L’opérateur instanceof s’utilise de la manière suivante :

objet instanceof Classe

Cet opérateur retourne true si l’objet est une instance de la classe spécifiée, ou d’une sous-classe de cette classe. Sinon, il retourne false.

Exemple d’Utilisation de l’Opérateur instanceof

Considérons un exemple où nous avons une classe Animal avec deux sous-classes Chien et Chat. Nous voulons vérifier le type réel d’un objet et agir en conséquence:

class Animal {}

class Chien extends Animal {}

class Chat extends Animal {}

public class Main {
    public static void main(String[] args) {
        Animal monAnimal = new Chien(); // Création d'un objet de type Chien

        // Vérification du type réel de monAnimal
        if (monAnimal instanceof Chien) {
            System.out.println("C'est un chien !");
        } else if (monAnimal instanceof Chat) {
            System.out.println("C'est un chat !");
        } else if (monAnimal instanceof Animal) {
            System.out.println("C'est un animal !");
        } else {
            System.out.println("Type inconnu !");
        }
    }
}

Dans cet exemple, nous créons un objet de type Chien et nous vérifions son type réel en utilisant l’opérateur instanceof. En fonction du résultat de cette vérification, nous affichons un message approprié.

Utilisation Pratique

L’opérateur instanceof est souvent utilisé en conjonction avec le polymorphisme pour effectuer des actions spécifiques en fonction du type réel des objets. Cela peut être utile dans des situations telles que la manipulation de collections d’objets de types différents, ou lors de la gestion de méthodes polymorphiques.

Travaux pratiques

Exercice 1 : Gestion d’Articles dans un Magasin

  1. Créez une classe Article avec les attributs reference, designation et prixUnitaire.
  2. Créez deux sous-classes de Article : ArticleAlimentaire et ArticleElectromenager. Ajoutez des attributs spécifiques comme datePeremption pour l’article alimentaire et garantie pour l’article électroménager.
  3. Implémentez une méthode afficherDetails() dans chaque sous-classe pour afficher les détails de l’article, y compris les attributs spécifiques.
  4. Créez une classe GestionMagasin avec une méthode main() pour tester vos implémentations. Créez des objets de type ArticleAlimentaire et ArticleElectromenager, appelez la méthode afficherDetails() et affichez les détails de chaque article.

Correction

1. Création de la classe Article :

public class Article {
    private String reference;
    private String designation;
    private double prixUnitaire;

    // Constructeur
    public Article(String reference, String designation, double prixUnitaire) {
        this.reference = reference;
        this.designation = designation;
        this.prixUnitaire = prixUnitaire;
    }

    // Getters et Setters
    public String getReference() {
        return reference;
    }

    public void setReference(String reference) {
        this.reference = reference;
    }

    public String getDesignation() {
        return designation;
    }

    public void setDesignation(String designation) {
        this.designation = designation;
    }

    public double getPrixUnitaire() {
        return prixUnitaire;
    }

    public void setPrixUnitaire(double prixUnitaire) {
        this.prixUnitaire = prixUnitaire;
    }

    // Méthode pour afficher les détails de l'article
    public void afficherDetails() {
        System.out.println("Référence : " + reference);
        System.out.println("Désignation : " + designation);
        System.out.println("Prix unitaire : " + prixUnitaire);
    }
}

2. Création de la sous-classes ArticleAlimentaire :

public class ArticleAlimentaire extends Article {
    private String datePeremption;

    // Constructeur
    public ArticleAlimentaire(String reference, String designation, double prixUnitaire, String datePeremption) {
        super(reference, designation, prixUnitaire);
        this.datePeremption = datePeremption;
    }

    // Getter et Setter
    public String getDatePeremption() {
        return datePeremption;
    }

    public void setDatePeremption(String datePeremption) {
        this.datePeremption = datePeremption;
    }

    // Méthode pour afficher les détails de l'article alimentaire
    @Override
    public void afficherDetails() {
        super.afficherDetails();
        System.out.println("Date de péremption : " + datePeremption);
    }
}

3. Création de la sous-classes ArticleElectromenager :

public class ArticleElectromenager extends Article {
    private int garantie;

    // Constructeur
    public ArticleElectromenager(String reference, String designation, double prixUnitaire, int garantie) {
        super(reference, designation, prixUnitaire);
        this.garantie = garantie;
    }

    // Getter et Setter
    public int getGarantie() {
        return garantie;
    }

    public void setGarantie(int garantie) {
        this.garantie = garantie;
    }

    // Méthode pour afficher les détails de l'article électroménager
    @Override
    public void afficherDetails() {
        super.afficherDetails();
        System.out.println("Garantie (en mois) : " + garantie);
    }
}

4. Création de la classe TestMagasin pour tester les implémentations :

public class TestMagasin {
    public static void main(String[] args) {
        // Création d'un article alimentaire
        ArticleAlimentaire articleAlimentaire = new ArticleAlimentaire("REF001", "Pain", 2.5, "10/01/2024");
        System.out.println("Détails de l'article alimentaire :");
        articleAlimentaire.afficherDetails();
        System.out.println();

        // Création d'un article électroménager
        ArticleElectromenager articleElectromenager = new ArticleElectromenager("REF002", "Lave-linge", 499.99, 24);
        System.out.println("Détails de l'article électroménager :");
        articleElectromenager.afficherDetails();
    }
}

Remarques :

  • Les classes Article, ArticleAlimentaire et ArticleElectromenager ont été correctement implémentées avec des attributs appropriés, des constructeurs, des méthodes getters/setters et des méthodes d’affichage.
  • La méthode afficherDetails() a été correctement implémentée dans chaque sous-classe pour afficher les détails spécifiques de l’article, tout en appelant la méthode afficherDetails() de la superclasse pour afficher les attributs communs.
  • La classe TestMagasin a été utilisée pour tester les implémentations en créant des objets d’article alimentaire et électroménager, puis en appelant la méthode afficherDetails() pour afficher les détails de chaque article.

Exercice2 : Gestion des Personnes dans une École – Implémentation Java

Description : Dans cet exercice, vous allez mettre en pratique vos compétences en programmation Java en implémentant les classes d’un système de gestion des personnes dans une école. Vous disposerez déjà du diagramme de classes UML représentant la structure de classes pour le système.

Diagramme de classes UML :

+---------------------------------+
|             Personne             |
+---------------------------------+
| - nom: String                   |
| - prenom: String                |
+---------------------------------+
| + afficherInfos(): void         |
+---------------------------------+
            ^
            |
            |
+----------------------+   
|       Etudiant       |   
+----------------------+   
| - matricule: String  |   
| - annee: int         |   
+----------------------+   
            ^
            |
            |
+----------------------+   
|      Enseignant      |   
+----------------------+   
| - diplome: String     |   
| - anciennete: int    |   
+----------------------+

Instructions :

  1. Implémentez les classes Java correspondant au diagramme de classes UML fourni. Vous aurez besoin des classes Personne, Etudiant et Enseignant.
  2. Respectez la relation d’héritage entre les classes. Par exemple, Etudiant et Enseignant doivent étendre la classe Personne.
  3. Ajoutez des attributs et des méthodes appropriés à chaque classe en vous basant sur le diagramme UML. Assurez-vous d’implémenter la méthode afficherInfos() dans chaque classe pour afficher les informations de chaque personne, étudiant ou enseignant.
  4. Dans la méthode main(), créez des instances d’étudiants et d’enseignants, appelez la méthode afficherInfos() pour chaque personne et affichez leurs informations.
  5. Testez votre implémentation avec différents étudiants et enseignants pour vérifier son bon fonctionnement.

Correction

Voici une implémentation détaillée en Java pour l’exercice donné :

  1. Classe Personne :
public class Personne {
    private String nom;
    private String prenom;

    public Personne(String nom, String prenom) {
        this.nom = nom;
        this.prenom = prenom;
    }

    public void afficherInfos() {
        System.out.println("Nom : " + nom);
        System.out.println("Prénom : " + prenom);
    }
}

Classe Etudiant :

public class Etudiant extends Personne {
    private String matricule;
    private int annee;

    public Etudiant(String nom, String prenom, String matricule, int annee) {
        super(nom, prenom);
        this.matricule = matricule;
        this.annee = annee;
    }

    @Override
    public void afficherInfos() {
        super.afficherInfos();
        System.out.println("Matricule : " + matricule);
        System.out.println("Année : " + annee);
    }
}

Classe Enseignant :

public class Enseignant extends Personne {
    private String diplome;
    private int anciennete;

    public Enseignant(String nom, String prenom, String diplome, int anciennete) {
        super(nom, prenom);
        this.diplome = diplome;
        this.anciennete = anciennete;
    }

    @Override
    public void afficherInfos() {
        super.afficherInfos();
        System.out.println("Diplôme : " + diplome);
        System.out.println("Ancienneté : " + anciennete + " ans");
    }
}

Méthode main() pour tester :

public class Main {
    public static void main(String[] args) {
        Etudiant etudiant = new Etudiant("Dupont", "Jean", "12345", 2022);
        Enseignant enseignant = new Enseignant("Smith", "Alice", "Doctorat en informatique", 5);

        System.out.println("Informations sur l'étudiant :");
        etudiant.afficherInfos();
        System.out.println("\nInformations sur l'enseignant :");
        enseignant.afficherInfos();
    }
}

Explication :

  • Nous avons créé une classe Personne avec des attributs nom et prenom, ainsi qu’une méthode afficherInfos() pour afficher les informations de la personne.
  • La classe Etudiant étend la classe Personne et ajoute les attributs matricule et annee. Elle redéfinit également la méthode afficherInfos() pour afficher les informations de l’étudiant.
  • La classe Enseignant fonctionne de la même manière que la classe Etudiant, mais avec des attributs diplome et anciennete.
  • Dans la méthode main(), nous créons des instances d’étudiant et d’enseignant, appelons la méthode afficherInfos() pour afficher leurs informations.

Interdire l’héritage ou la redéfinition de méthode

Pour interdire l’héritage ou la redéfinition de méthode dans Java, vous pouvez utiliser le mot-clé final. Voici comment vous pouvez l’appliquer :

  1. Pour interdire l’héritage d’une classe : Vous pouvez déclarer la classe avec le mot-clé final. Cela signifie que la classe ne peut pas être étendue par d’autres classes.Exemple :
public final class MaClasse {
    // Corps de la classe
}

Pour interdire la redéfinition d’une méthode : Vous pouvez déclarer la méthode avec le mot-clé final. Cela signifie que la méthode ne peut pas être redéfinie par les sous-classes.

Exemple :

public class MaClasse {
    public final void maMethode() {
        // Corps de la méthode
    }
}

Dans les deux cas, l’utilisation du mot-clé final garantit que la classe ou la méthode ne peut pas être modifiée ou étendue, ce qui peut être utile dans certaines situations où vous voulez contrôler strictement le comportement de votre code.

Voici un exemple où l’interdiction d’héritage est appliquée en Java :

public final class Animal {
    public void manger() {
        System.out.println("L'animal mange.");
    }
}

// La classe Chien tente d'étendre la classe Animal, mais elle ne peut pas
// car Animal est déclarée comme final.
public class Chien extends Animal {
    // Ceci générera une erreur de compilation :
    // "Cannot inherit from final Animal"
}

Dans cet exemple, la classe Animal est déclarée comme final, ce qui signifie qu’elle ne peut pas être étendue par d’autres classes. Par conséquent, lorsque nous essayons de créer une classe Chien qui étend Animal, une erreur de compilation se produira car l’héritage de la classe Animal est interdit en raison de sa déclaration comme final.

Les Classes Scellées

Les classes scellées sont une nouvelle fonctionnalité introduite dans Java à partir de la version 15. Elles permettent de restreindre l’héritage en spécifiant explicitement quelles sous-classes sont autorisées pour une classe donnée. Cela offre un contrôle précis sur la hiérarchie des classes et garantit que seules certaines sous-classes spécifiées peuvent être créées.

Déclaration d’une Classe Scellée :

Pour déclarer une classe comme scellée, utilisez le mot-clé sealed devant la déclaration de classe.

public sealed class Animal permits Chien, Chat {
    // Corps de la classe Animal
}

Dans cet exemple, Animal est une classe scellée qui permet deux sous-classes spécifiques : Chien et Chat.

Déclaration des Sous-classes Autorisées :

Utilisez le mot-clé permits pour spécifier les sous-classes autorisées pour une classe scellée.

public final class Chien extends Animal {
    // Corps de la classe Chien
}

public non-sealed class Chat extends Animal {
    // Corps de la classe Chat
}

Dans cet exemple, Chien est une sous-classe autorisée de Animal, déclarée comme final, ce qui signifie qu’elle ne peut pas être étendue davantage. Chat est également une sous-classe autorisée, mais elle est déclarée comme non-sealed, ce qui signifie qu’elle peut avoir des sous-classes supplémentaires non spécifiées ici.

Utilisation des Classes Scellées :

Les classes scellées offrent un contrôle précis sur l’héritage et sont utiles lorsque vous souhaitez restreindre la création de sous-classes pour une classe donnée. Elles garantissent que la hiérarchie des classes reste conforme à la conception et facilite la maintenance du code en réduisant le risque de mauvaise utilisation des sous-classes.

Exemple d’Exercice Corrigé :

Exercice : Gestion des Véhicules – Implémentation Java avec Classes Scellées

Dans cet exercice, vous devez créer un système de gestion de véhicules en utilisant des classes scellées pour restreindre l’héritage.

  1. Déclarez une classe scellée Vehicule qui autorise les sous-classes Voiture et Moto.
  2. Créez les sous-classes Voiture et Moto avec des attributs et des méthodes appropriés.
  3. Implémentez une méthode demarrer() dans chaque sous-classe pour démarrer le véhicule.
  4. Créez une nouvelle sous-classe Avion en dehors du paquetage actuel et essayez de la faire hériter de Vehicule. Cela devrait générer une erreur de compilation en raison de la restriction imposée par la classe scellée.

Correction

package gestionvehicules;

public sealed class Vehicule permits Voiture, Moto {
    // Corps de la classe Vehicule
}

public final class Voiture extends Vehicule {
    public void demarrer() {
        System.out.println("La voiture démarre !");
    }
}

public final class Moto extends Vehicule {
    public void demarrer() {
        System.out.println("La moto démarre !");
    }
}

// La classe Avion est située dans un paquetage différent
package autresvehicules;

// Cette tentative d'héritage de la classe Vehicule générera une erreur de compilation
public class Avion extends gestionvehicules.Vehicule {
    // Corps de la classe Avion
}

public class Main {
    public static void main(String[] args) {
        Vehicule maVoiture = new Voiture();
        Vehicule maMoto = new Moto();

        maVoiture.demarrer(); // Affiche : La voiture démarre !
        maMoto.demarrer(); // Affiche : La moto démarre !
    }
}

Dans cet exemple, nous avons remplacé les animaux par des véhicules. Les classes Voiture et Moto sont maintenant des sous-classes de Vehicule, qui est une classe scellée. Nous avons également ajouté une nouvelle classe Avion dans un paquetage différent et tenté de la faire hériter de Vehicule, ce qui génère une erreur de compilation. Cela illustre la restriction imposée par les classes scellées.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *