Skip to content

TD / TP Java 2 : Héritage, Polymorphisme et Abstraction

Objectifs et Prérequis

Prérequis : Avoir assimilé les concepts du TD 1 (Classe, Objet, Encapsulation, static, Types primitifs vs Objets).

Objectifs de la séance (3h) :

  1. Révisions : Consolider l'utilisation du mot-clé this, des constructeurs, de la méthode main() et des entrées utilisateur (Scanner).
  2. Nouveautés : Découvrir la puissance de la POO à travers l'Héritage, le Polymorphisme, les Classes abstraites et les Interfaces.
  3. Outils : Commencer sur BlueJ, puis basculer dans le terminal pour utiliser une bibliothèque graphique externe multi-robots (.jar).

Étape 1 : Révisions - Constructeurs, this et Scanner (Sous BlueJ)

📖 Notion de cours : Constructeur et le mot-clé this

Au TD précédent, nos objets naissaient "vides". Le Constructeur permet de forcer un état initial au moment du new. Il porte exactement le nom de la classe. On utilise this pour désigner "l'attribut de l'objet courant" afin d'éviter la confusion avec les paramètres.

💻 Exercice 1 : Un Robot interactif et mobile

  1. Lancez BlueJ et créez un nouveau projet FlotteRobotique.
  2. Créez une classe Robot. Ajoutez-lui ces attributs privés : String nom, int batterie (pourcentage), int x et int y (coordonnées).
  3. Le Constructeur : Ajoutez le code suivant pour forcer l'initialisation :
    public Robot(String nom) {
        this.nom = nom; // 'this.nom' est l'attribut, 'nom' est le paramètre
        this.batterie = 100;
        this.x = 200; // Position centrale par défaut
        this.y = 200;
    }
    
  4. Ajoutez les Getters classiques pour tous ces attributs (getNom(), getBatterie(), getX(), getY()).
  5. Ajoutez une méthode public void deplacer(int dx, int dy) qui modifie x et y en y ajoutant dx et dy, et qui diminue la batterie de 1.
  6. Ajoutez une méthode public void afficherStatut() qui affiche dans la console le nom, la batterie et la position du robot.

📖 Notion de cours : Entrées / Sorties avec Scanner

Pour lire les entrées de l'utilisateur, on utilise la classe Scanner. Il faut l'importer avec import java.util.Scanner; tout en haut du fichier. Prenez le réflexe d'ouvrir la documentation officielle pour découvrir toutes les méthodes qu'elle propose !

  1. Créez une nouvelle classe Main contenant la méthode point d'entrée du programme :
    import java.util.Scanner;
    
    public class Main {
        public static void main(String[] args) {
            Scanner clavier = new Scanner(System.in);
    
            System.out.print("Entrez le nom de votre nouveau robot : ");
            String nomSaisi = clavier.nextLine();
    
            clavier.close();
    
            // TODO : Instanciez un nouveau Robot en utilisant 'nomSaisi'
            // TODO : Appelez la méthode afficherStatut() de votre robot
    
        }
    }
    
  2. Testez : Dans BlueJ, compilez, faites un clic droit sur la classe Main et lancez void main(String[] args).

Étape 2 : L'Héritage - Étendre les capacités

📖 Notion de cours : L'Héritage et protected

L'Héritage permet de créer une nouvelle classe basée sur une classe existante avec le mot-clé extends. Problème : Les attributs private du parent sont inaccessibles, même pour l'enfant ! Solution : Le modificateur protected. Il agit comme private pour l'extérieur, mais autorise l'accès aux classes enfants.

💻 Exercice 2 : La naissance du Drone

  1. Dans la classe Robot, changez la visibilité de TOUS vos attributs : remplacez private par protected.
  2. Créez une classe Drone héritant de Robot. Ajoutez-lui un attribut propre : protected int altitude;
  3. Le constructeur de l'enfant : Un enfant doit obligatoirement appeler le constructeur de son parent. On utilise super() sur la première ligne :
    public Drone(String nom) {
        super(nom); // Appelle le constructeur de Robot
        this.altitude = 0;
    }
    
  4. Ajoutez une méthode public void decoller() qui augmente l'altitude de 10 et diminue la batterie de 5.

📖 Notion de cours : La Redéfinition et l'annotation @Override

La redéfinition (overriding) consiste, pour une classe enfant, à réécrire une méthode héritée de son parent pour en modifier ou en spécialiser le comportement.

L'annotation @Override s'écrit juste au-dessus de la méthode redéfinie. Elle n'est pas techniquement obligatoire, mais elle est fortement recommandée car :

  1. Elle sécurise le code : Le compilateur vérifiera que la méthode existe bien chez le parent. Si vous faites une faute de frappe (ex: bouger au lieu de deplacer) ou une erreur dans les paramètres, il bloquera la compilation au lieu de créer silencieusement une nouvelle méthode.
  2. Elle améliore la lisibilité : Elle indique clairement aux autres développeurs que vous êtes en train d'altérer un comportement standard.

💻 Exercice 3 : À vous de jouer ! Le Rover terrestre

Créez un nouveau type de robot sans aide !

  1. Créez une classe Rover qui hérite de Robot.
  2. Ajoutez-lui un attribut privé : int distanceParcourue.
  3. Écrivez son constructeur (pensez à super()).
  4. Redéfinissez (@Override) sa méthode deplacer(int dx, int dy) : elle doit appeler super.deplacer(dx, dy) afin de bouger réellement (et consommer de la batterie), mais elle doit en plus incrémenter distanceParcourue !

Étape 3 : Polymorphisme et la méthode toString()

📖 Notion de cours : Object et la méthode toString()

En Java, toutes les classes héritent implicitement d'une classe originelle : Object. N'hésitez pas à consulter la documentation officielle de Object pour voir les méthodes qu'elle offre. L'une d'elles est public String toString(). Java l'appelle automatiquement lorsque vous faites System.out.println(monObjet);. L'idée est de la redéfinir (@Override) dans nos classes pour afficher des informations utiles.

💻 Exercice 4 : L'art du déguisement

  1. Dans Robot, remplacez afficherStatut() par la redéfinition de toString() :
    @Override
    public String toString() {
        return "ROBOT " + nom + " | Bat: " + batterie + "% | Pos: [" + x + "," + y + "]";
    }
    
  2. Dans Drone, redéfinissez aussi toString() pour inclure l'altitude, en réutilisant le code du parent via super.toString().
  3. (Optionnel) Faites de même dans Rover pour inclure la distance parcourue.
  4. Retournez dans votre classe Main. Effacez le contenu du main() précédent. Nous allons créer un tableau contenant différents types d'objets :
    public class Main {
        public static void main(String[] args) {
            // Un tableau de 3 références "Robot"
            Robot[] flotte = new Robot[3]; 
    
            flotte[0] = new Robot("R2D2");
            flotte[1] = new Drone("Mavic"); // Polymorphisme ! Un Drone EST un Robot.
            flotte[2] = new Rover("Curiosity"); 
    
            for(int i = 0; i < flotte.length; i++) {
                // Observez la magie du polymorphisme couplée à toString() :
                System.out.println(flotte[i]); 
            }
        }
    }
    

Étape 4 : Le Poste de Commande Multi-Robots (Terminal)

Fermez BlueJ. Nous passons dans le terminal pour compiler nos fichiers manuellement et intégrer une bibliothèque externe graphique.

💻 Exercice 5 : Mise en peinture (Refactoring)

Avant de lancer l'interface graphique, nos robots ont besoin d'une couche de peinture ! Dans l'exercice suivant, nous allons vous fournir une interface FenetreRobot qui s'attend en effet à pouvoir lire la couleur de chaque robot pour les dessiner à l'écran.

C'est à vous de modifier l'architecture de votre code pour intégrer cette nouveauté :

  1. Dans Robot.java, importez la classe java.awt.Color tout en haut du fichier. Consultez sa documentation pour voir comment utiliser les couleurs pré-définies (les constantes comme Color.RED, Color.BLUE, etc.).
  2. Ajoutez un attribut couleur de type Color à votre classe Robot. Réfléchissez à sa visibilité : public, protected ou private ?
  3. Modifiez le constructeur de Robot pour qu'il prenne un deuxième paramètre de type Color et initialise l'attribut.
  4. Ajoutez le getter correspondant : public Color getCouleur().
  5. Attention : Si vous recompilez maintenant, vous aurez des erreurs dans Drone et Rover ! C'est normal : le constructeur parent a changé. Modifiez les constructeurs de Drone et Rover pour qu'ils acceptent aussi une couleur en paramètre, et passez-la correctement au parent via super(..., ...);.

📖 Notion de cours : La Surcharge (Overloading)

La surcharge consiste à définir dans une même classe plusieurs méthodes (ou constructeurs) qui portent le même nom, mais qui diffèrent par leurs paramètres (nombre, type ou ordre).

  • L'objectif : Offrir de la souplesse à l'utilisateur de votre classe en proposant plusieurs façons de faire la même action. Par exemple, System.out.println() est surchargée : on peut lui donner un int, une String ou un Objet.
  • La règle : Le compilateur sait quelle méthode exécuter en analysant les arguments que vous lui passez. Attention : changer uniquement le type de retour d'une méthode ne suffit pas à la surcharger !

💻 Exercice 6 : La Surcharge à la rescousse

Dans l'exercice précédent, nous avons forcé tous les robots à prendre une couleur à la création. Mais que se passe-t-il si un utilisateur veut créer un robot sans se soucier de sa couleur ? Nous allons surcharger le constructeur et la méthode de déplacement !

  1. Surcharge du Constructeur : Dans la classe Robot, ajoutez un deuxième constructeur qui ne prend que le nom en paramètre. À l'intérieur, plutôt que de réécrire toute l'initialisation, appelez l'autre constructeur en lui passant une couleur par défaut (par exemple gris) en utilisant la syntaxe this(...) sur la première ligne :
    // Nouveau constructeur surchargé
    public Robot(String nom) {
        // Appelle le constructeur principal de cette même classe
        this(nom, Color.GRAY); 
    }
    
  2. Surcharge de méthode : Actuellement, deplacer(int dx, int dy) consomme toujours 1 point de batterie. Créez une nouvelle méthode public void deplacer(int dx, int dy, int coutEnergie) dans la classe Robot.
  3. Copiez la logique de déplacement dans cette nouvelle méthode pour qu'elle déduise coutEnergie de la batterie au lieu de 1.
  4. Optimisation pro : Modifiez votre ancienne méthode deplacer(int dx, int dy) pour qu'elle se contente d'appeler la nouvelle : this.deplacer(dx, dy, 1);. C'est le principe du code "DRY" (Don't Repeat Yourself).
  5. Testez : Dans le Main, instanciez un robot sans préciser de couleur, et faites-le se déplacer en utilisant la méthode à 3 paramètres (par exemple un sprint qui coûte 10 d'énergie).

💻 Exercice 7 : Interface Graphique

Téléchargez le fichier FenetreRobot.jar. Il contient une interface graphique capable d'afficher votre flotte de robots. Elle dessinera un cercle pour les Robot simples, un triangle pour les Drone, et un carré pour les Rover, le tout dans la couleur de l'objet !

  1. Naviguez dans votre terminal vers le dossier de votre projet. Placez le fichier FenetreRobot.jar dans ce dossier.
  2. Modifiez votre fichier Main.java pour tester l'interface. Instanciez plusieurs robots avec des couleurs (observez l'import de Color) dans un tableau passez ce tableau à l'interface graphique :
    import java.awt.Color;
    
    public class Main {
        public static void main(String[] args) {
            // On crée un tableau capable de contenir 3 références de Robots
            Robot[] maFlotte = new Robot[3];
    
            // Les constructeurs exigent maintenant une couleur !
            maFlotte[0] = new Robot("Standard-1", Color.BLACK);
            maFlotte[1] = new Drone("SkyWatcher", Color.RED);
            maFlotte[2] = new Rover("Wall-E", new Color(255, 165, 0)); // Code RGB pour le orange
    
            // On lance l'interface graphique contenue dans le .jar en lui confiant notre tableau
            // (Attention : le compilateur BlueJ ne connaîtra pas cette classe, il faut compiler dans le terminal).
            FenetreRobot posteDeCommande = new FenetreRobot(maFlotte);
        }
    }
    

📖 Notion de cours : Le Classpath (-cp) et les archives .jar

Une archive .jar (Java ARchive) est un fichier regroupant des classes pré-compilées fournies par d'autres développeurs. Pour utiliser une bibliothèque externe, il faut indiquer au compilateur (javac) et à la machine virtuelle (java) où la trouver via l'option -cp (Classpath).

  1. Compilation : Compilez votre programme en incluant le .jar dans le classpath.
    • Windows : javac -cp ".;FenetreRobot.jar" *.java
    • Mac/Linux : javac -cp ".:FenetreRobot.jar" *.java
  2. Exécution : Lancez la machine virtuelle.
    • Windows : java -cp ".;FenetreRobot.jar" Main
    • Mac/Linux : java -cp ".:FenetreRobot.jar" Main
  3. Amusez-vous à déplacer vos robots. Utilisez le sélecteur en bas à gauche pour choisir le robot à contrôler.

  4. Le bug de l'énergie infinie : Continuez de déplacer un robot jusqu'à ce que sa batterie atteigne 0%. Que se passe-t-il si vous cliquez encore ? Le robot continue de bouger et sa batterie devient négative ! C'est inadmissible pour une simulation réaliste.

📖 Notion de cours : Les flux de sortie (System.out vs System.err)

Jusqu'à présent, vous avez utilisé System.out.println() pour le flux de sortie standard. La classe System fournit un flux spécifiquement dédié aux alertes et erreurs : System.err.println(). La plupart des consoles afficheront ce texte en rouge pour alerter le développeur !

  1. Correction : Retournez dans le code de Robot (et Rover). Modifiez la méthode deplacer(...) avec un if. Le robot ne doit bouger et consommer de l'énergie que si sa batterie est strictement positive. Sinon, affichez un message d'erreur rouge avec System.err.println(...).
  2. Cloué au sol : Appliquez exactement la même logique à votre classe Drone. Modifiez sa méthode decoller() pour qu'elle vérifie l'état de la batterie avant d'autoriser le décollage, et affiche une erreur en rouge dans le cas contraire.
  3. Recompilez vos classes et testez à nouveau avec l'interface graphique. Observez la console lorsque la batterie de vos robots (et de votre drone) tombe à zéro !

Étape 5 : L'Abstraction (Toujours dans le Terminal)

📖 Notion de cours : Classes Abstraites, Interfaces et l'Interdiction de l'Héritage Multiple

  • Classe Abstraite (abstract class) : Classe "incomplète" contenant des méthodes abstraites (sans code). On ne peut pas faire de new dessus. Elle sert de base pour l'héritage.
  • Interface (interface) : Contrat pur sans aucun attribut d'état ni code implémenté.

⚠️ RÈGLE D'OR : En Java, l'héritage multiple est interdit ! On ne peut faire extends que d'une seule classe mère. Les Interfaces permettent d'outrepasser cette limite car une classe peut faire implements de plusieurs interfaces simultanément !

💻 Exercice 7 : Les Contrats Logiciels et le Compilateur

  1. Restez dans votre terminal. Créez un fichier Connectable.java :

    public interface Connectable {
        void seConnecterReseau();
        void seDeconnecter();
    }
    

  2. Créez un fichier Machine.java :

    public abstract class Machine {
        protected String numeroSerie;
    
        public Machine(String numeroSerie) {
            this.numeroSerie = numeroSerie;
        }
    
        public abstract void demarrer(); // Méthode sans code, obligera les enfants à l'écrire
    }
    

  3. Confrontation avec le compilateur : Créez StationMeteo.java qui hérite de l'un et implémente l'autre, mais ne lui donnez que son constructeur pour le moment :

    public class StationMeteo extends Machine implements Connectable {
        public StationMeteo(String num) {
            super(num);
        }
    }
    

  4. Dans le terminal, tentez de compiler : javac StationMeteo.java. Lisez attentivement le message d'erreur. Que vous réclame le compilateur ?
  5. Corrigez StationMeteo.java en ajoutant le code (même basique avec des System.out.println) des trois méthodes obligatoires manquantes (demarrer(), seConnecterReseau() et seDeconnecter()). Compilez de nouveau jusqu'à ce que l'erreur disparaisse.
  6. À vous de jouer : Sans modèle cette fois, créez un fichier CapteurThermique.java. Un capteur thermique est une Machine, mais il n'est pas Connectable. Écrivez sa classe, son constructeur, et implémentez l'unique méthode obligatoire. Compilez pour vérifier.

💻 Exercice 8 : Mise en orbite

Créez enfin un MainSysteme.java pour tester l'ensemble :

public class MainSysteme {
    public static void main(String[] args) {
        StationMeteo sm = new StationMeteo("SM-Alpha");
        sm.demarrer();

        // Polymorphisme avec les Interfaces !
        // Je peux stocker ma station dans une référence "Connectable" car elle respecte le contrat
        Connectable appareilReseau = sm;
        appareilReseau.seConnecterReseau(); 

        // TODO : Instanciez un CapteurThermique
        // TODO : Appelez sa méthode pour le démarrer
    }
}

Compilez tout avec javac *.java (ici pas besoin de -cp car ce programme n'utilise pas l'interface graphique), puis exécutez avec java MainSysteme.


📝 Auto-évaluation Interactive

Cochez la bonne réponse pour vérifier vos acquis.

1. À quoi sert le mot-clé super() dans le constructeur d'une classe enfant ?



2. Quelle est la principale différence entre une Interface et une Classe Abstraite en Java ?



3. Soit le code : Machine m = new StationMeteo();. Comment appelle-t-on ce principe ?



4. Pourquoi utilise-t-on la visibilité protected sur les attributs d'une classe parent ?



5. Que se passe-t-il si vous passez un objet directement à System.out.println() sans avoir redéfini la méthode toString() ?



6. Dans le terminal, à quoi sert l'option -cp (ou -classpath) lors de la compilation ?



7. Quel est l'intérêt d'utiliser System.err.println() plutôt que System.out.println() pour les erreurs ?



8. En Java, qu'est-ce que la redéfinition (overriding) d'une méthode ?



9. Que se passe-t-il si vous redéfinissez correctement une méthode du parent dans la classe enfant, mais que vous oubliez d'écrire l'annotation @Override au-dessus ?



10. Surcharge ou Redéfinition ? Observez ces deux méthodes :
Dans la classe parente Robot : public void recharger()
Dans la classe enfant Drone extends Robot : public void recharger()



11. Surcharge ou Redéfinition ? Observez ces deux méthodes au sein de la MÊME classe Robot :
public void deplacer(int distance)
public void deplacer(int dx, int dy)



12. Le piège classique : Observez ces deux méthodes au sein de la MÊME classe :
public int calculerEnergie()
public double calculerEnergie()




Sujet rédigé à l'aide de Gemini (pas par Gemini)