Skip to content

TD Machine 3 : Les Collections et l'Aquarium Connecté

Introduction

Bienvenue dans ce troisième TD ! Jusqu'à présent, pour stocker plusieurs objets, nous utilisions des tableaux (Robot[], int[]). Bien que très performants, les tableaux ont un défaut majeur : leur taille est fixe. Si vous créez un tableau de 10 cases, impossible d'y ajouter un 11ème élément sans recréer un nouveau tableau. Aujourd'hui, nous allons découvrir les Collections Java en animant un aquarium virtuel !


Échauffement : Plongeon dans la POO

Pour ce TD, téléchargez le fichier AquariumGUI.jar. Ce simulateur se chargera d'animer vos objets en temps réel à l'écran, à condition que vous respectiez un contrat de nommage précis pour vos méthodes.

Exercice 1 : Entités Aquatiques et Animation

  1. Créez une classe abstraite EntiteAquatique avec :

    • Des attributs protégés : nom (String), x et y (int), et vitesse (int).
    • Un constructeur pour initialiser tout cela.
    • Important pour la fenêtre : Ajoutez les méthodes publiques getX(), getY() et getNom().
    • Déclarez une méthode abstraite public abstract void seDeplacer();.
  2. Créez une classe Poisson héritant de EntiteAquatique.

    • Son déplacement : il avance en x de 1 * vitesse. Si x dépasse 800 (le bord de l'écran), remettez x à 0.
  3. Créez une classe SousMarin héritant de EntiteAquatique.

    • Son déplacement : il avance en x de 1 * vitesse et oscille de bas en haut (modifiez légèrement le y).

Démarrer l'animation

Dans votre classe Main, passez un tableau polymorphique au simulateur visuel :

import gui.SimulateurAquarium;

public class Main {
    public static void main(String[] args) {
        EntiteAquatique[] monBassin = new EntiteAquatique[3];
        monBassin[0] = new Poisson("Nemo", 10, 50, 5); // Vitesse rapide !
        monBassin[1] = new Poisson("Dory", 15, 150, 2); // Plus lente
        monBassin[2] = new SousMarin("Nautilus", 5, 300, 3);

        SimulateurAquarium fenetre = new SimulateurAquarium();
        fenetre.demarrerAnimation(monBassin); // Lance la boucle d'animation
    }
}
(Compilation sous linux (pour windows remplacer : par ;) : javac -cp ".:AquariumGUI.jar" *.java)


Lesi Collections et les Génériques

Votre aquarium tourne, mais un tableau est figé. Si l'on veut simuler des naissances en cours d'exécution, il nous faut une structure dynamique.

Cours : Les Listes et leurs implémentations

L'interface List représente une collection ordonnée qui grandit automatiquement. On utilise le polymorphisme pour l'instancier avec l'une de ses deux implémentations principales :

  • ArrayList : Fonctionne en interne comme un tableau redimensionnable. Très rapide pour lire les éléments, mais plus lente pour des insertions massives au milieu. C'est le choix par défaut.
  • LinkedList : Une liste chaînée. Très rapide pour ajouter/supprimer des éléments en cours de route, mais plus lente pour lire un élément au hasard.

Cours : La sécurité grâce aux Génériques < >

Avant Java 5, les collections stockaient de simples instances de la classe Object. Rien n'empêchait d'ajouter par erreur un objet imprévu (un entier, un robot) dans une liste de Poissons, causant un crash brutal à l'exécution ! Aujourd'hui, l'utilisation des Génériques (ex: List<Poisson>) force le compilateur à vérifier rigoureusement les types.

Cours : La boucle for-each

Pour parcourir facilement une collection sans utiliser d'indice i, on utilise la boucle for-each :

List<String> prenoms = new ArrayList<>();
prenoms.add("Alice");
prenoms.add("Bob");

// "Pour chaque String 'p' dans la liste 'prenoms'..."
for (String p : prenoms) {
    System.out.println(p);
}

Exercice 2 : L'Aquarium Dynamique

  1. Dans votre Main, remplacez le tableau par une List<EntiteAquatique>.
  2. Utilisez la méthode .add() pour insérer vos poissons.
  3. Modifiez l'appel à la fenêtre pour lui passer la liste : fenetre.demarrerAnimation(maListe);. La fenêtre est capable de lire aussi bien des tableaux que des Listes car ces objets implémentent l'interface Iterable !

L'identité des objets : Le piège de l'égalité

Exercice 3 : Le paradoxe des clones

Dans votre Main, testez ce code et observez le résultat dans la console. Notez bien que les deux poissons sont créés avec exactement les mêmes valeurs.

Poisson p1 = new Poisson("Clown", 10, 10, 2);
Poisson p2 = new Poisson("Clown", 10, 10, 2); 

System.out.println("Avec ==      : " + (p1 == p2));
System.out.println("Avec equals  : " + p1.equals(p2));

Vous devriez obtenir false dans les deux cas ! Pourquoi deux poissons partageant les mêmes caractéristiques ne sont-ils pas considérés comme égaux ?

Cours : == vs equals()

  • L'opérateur == vérifie si deux variables pointent vers la même adresse mémoire. Ici, nous avons deux instances distinctes (deux new).
  • La méthode equals(), héritée de Object, compare le contenu. Mais par défaut, elle se contente de faire un == !

Exercice 4 : Redéfinir l'égalité

  1. Dans votre classe Poisson, redéfinissez (Override) la méthode equals().
  2. Faites en sorte que deux poissons soient considérés égaux s'ils ont le même nom.
  3. Relancez l'exercice 3 : p1.equals(p2) doit maintenant renvoyer true !

L'interface Set et l'importance du hashCode

Cours : Les Ensembles (Set)

L'interface Set représente une collection qui refuse les doublons. Si vous ajoutez un élément déjà présent (au sens de equals()), il sera ignoré. L'implémentation la plus courante est HashSet.

Exercice 5 : Le Set défectueux à l'écran

Voyons cela visuellement. Dans votre Main :

  1. Pour bien voir les deux poissons à l'écran, ajoutez une méthode public void setY(int y) dans la classe EntiteAquatique.
  2. Décalez le deuxième poisson vers le bas : p2.setY(80);.
  3. Créez un Set<EntiteAquatique> especes = new HashSet<>();.
  4. Ajoutez-y vos poissons p1 et p2.
  5. Passez ce Set à la fenêtre : fenetre.demarrerAnimation(especes);.

Surprise à l'écran ! Vous devriez voir DEUX poissons avancer en parallèle. Le Set a conservé le doublon !

Cours : Le contrat equals / hashCode

Le HashSet est optimisé. Plutôt que de comparer votre poisson avec tous les autres via equals(), il génère une empreinte numérique : le hashCode().

Il range les objets dans des "tiroirs" numérotés. S'il ne trouve personne dans le tiroir, il ajoute l'objet.

Le Contrat : Si deux objets sont égaux (equals()), ils DOIVENT avoir le même hashCode(). Si vous redéfinissez equals sans redéfinir hashCode, vos deux poissons iront dans des tiroirs différents et le Set les gardera tous les deux !

Exercice 6 : Réparer la simulation

  1. Dans Poisson, générez/redéfinissez la méthode hashCode() en vous basant uniquement sur l'attribut nom.
  2. Relancez. Le deuxième poisson a disparu de l'écran ! Le Set fonctionne enfin.

Égalité stricte : Un nom ne fait pas tout !

Dans l'exercice précédent, nous avons décidé que deux poissons étaient "égaux" s'ils avaient le même nom. Mais est-ce vraiment logique ? Suite à votre appel à setY(80), p1 est en position y=10 et p2 en position y=80. Ce sont techniquement deux poissons distincts qui nagent à des endroits différents !

Cours : Générer un hashCode efficacement

Pour définir rigoureusement l'identité d'un objet, on doit inclure tous ses attributs pertinents dans equals() et hashCode(). Calculer un identifiant numérique à partir de multiples attributs peut être fastidieux. Heureusement, Java fournit un outil magique : java.util.Objects.hash(attr1, attr2...).

Exercice 7 : Restituer la vérité

  1. Retournez dans votre classe Poisson.
  2. Modifiez equals() pour qu'il compare tous les attributs (nom, x, y, vitesse).
  3. Modifiez hashCode() en utilisant Objects.hash(nom, x, y, vitesse).
  4. Relancez la simulation.

Bilan : Vos deux poissons réapparaissent à l'écran ! C'est normal : puisque leurs y sont différents, ce ne sont plus des clones parfaits. (C'était volontaire de vous faire changer le y pour voir ce comportement !)


Les Dictionnaires : Map et HashMap

Cours : L'association Clé-Valeur

L'interface Map permet d'associer une "Clé" unique à une "Valeur". L'implémentation standard est HashMap.

Exercice 8 : L'interface graphique des bassins

Notre fenêtre graphique affiche des boutons de navigation si on lui fournit une Map !

  1. Créez une Map<String, Iterable<EntiteAquatique>> parc = new HashMap<>();.
  2. Créez deux listes (ou Sets) : de Poissons, et de Sous-Marins.
  3. Ajoutez-les dans la Map (clés : "Bassin Tropical", "Zone Abyssale").
  4. Donnez la Map entière à la fenêtre via demarrerAnimation(parc).

🚀 Exercice Bonus : Le Requin et l'Exception Concurrente

(À faire uniquement si vous avez terminé le reste du TD !)

Un requin s'est introduit dans le Bassin Tropical ! Il va dévorer tous les poissons qui sont trop lents.

Exercice Bonus : La sélection naturelle

  1. Dans le Main, créez une liste ArrayList<EntiteAquatique> avec beaucoup de poissons, certains rapides (vitesse > 3) et d'autres lents.
  2. Créez une méthode statique attaquerBassin(List<EntiteAquatique> bassin).
  3. Dans cette méthode, utilisez une boucle for-each pour parcourir le bassin. Si la vitesse de l'entité est inférieure à 3 (ajoutez un getVitesse() dans EntiteAquatique), supprimez-la de la liste avec bassin.remove(entite).
  4. Appelez cette méthode puis lancez l'animation.

CRASH ! Vous devriez obtenir une belle ConcurrentModificationException.

Pourquoi ça plante ?

En Java, il est strictement interdit de modifier la structure d'une collection (ajouter ou supprimer des éléments) pendant qu'on la parcourt avec une boucle for-each. Le compteur interne de la collection s'y perd !

Pour corriger, la méthode à l'ancienne (avant Java 8) : Utiliser un Iterator Iterator<EntiteAquatique> it = bassin.iterator(); avec une boucle ex: while(it.hasNext()). Pour supprimer, on appelle it.remove() (c'est l'itérateur qui supprime de manière sécurisée, pas la liste directement !).

Cours : Les Expressions Lambda (Java 8+)

Apparues avec Java 8, les expressions lambda permettent d'écrire du code de manière beaucoup plus concise. Elles sont utiles lorsqu'on veut passer un "comportement" en paramètre d'une méthode.

Syntaxe générale : (parametres) -> { instructions; }

Si vous n'avez qu'un seul paramètre, la syntaxe devient ultra minimaliste : param -> condition.

La méthode moderne pour supprimer des éléments en toute sécurité utilise la méthode removeIf() présente sur toutes les collections.

Exemple :

// "Pour chaque entité 'e', si sa vitesse est inférieure à 3, on la supprime"
bassin.removeIf(e -> e.getVitesse() < 3);

Correction du Bonus

  1. Remplacez la boucle for-each par une boucle for ou while avec un Iterator et utilisez la méthode remove() de ce dernier.
  2. Relancez l'aquarium : seuls les poissons les plus rapides ont survécu !
  3. Effacez finalement votre boucle et remplacez-la par un appel à removeIf en utilisant une expression lambda : code beaucoup plus concis.

🚀 Exercice Bonus : Le Requin et l'Exception Concurrente

(À faire uniquement si vous avez terminé le reste du TD !)

Un requin s'est introduit dans le Bassin Tropical ! Il va dévorer tous les poissons qui sont trop lents.

Exercice Bonus : La sélection naturelle

  1. Dans le Main, créez une liste ArrayList<EntiteAquatique> avec beaucoup de poissons, certains rapides (vitesse > 3) et d'autres lents.
  2. Créez une méthode statique attaquerBassin(List<EntiteAquatique> bassin).
  3. Dans cette méthode, utilisez une boucle for-each pour parcourir le bassin. Si la vitesse de l'entité est inférieure à 3, supprimez-la de la liste avec bassin.remove(entite).
  4. Appelez cette méthode puis lancez l'animation.

CRASH ! Vous devriez obtenir une belle ConcurrentModificationException.

Pourquoi ça plante et comment corriger ?

En Java, il est strictement interdit de modifier la structure d'une collection (ajouter ou supprimer des éléments) pendant qu'on la parcourt avec une boucle for-each. Le compteur interne de la collection s'y perd !

Pour corriger, deux solutions :

  1. À l'ancienne (avant Java 8) : l'Iterator. Utilisez un Iterator<EntiteAquatique> it = bassin.iterator(); avec une boucle while(it.hasNext()). Pour supprimer, appelez it.remove() (c'est l'itérateur qui supprime de manière sécurisée, pas la liste directement !).
  2. La méthode moderne (Java 8+) : Utilisez la méthode removeIf présente sur toutes les listes. Cherchez comment l'utiliser avec une expression Lambda !

🧠 Testez vos connaissances !

1. Quelle est la principale différence entre une List et un Set ?

2. Que se passe-t-il si je redéfinis equals() sans redéfinir hashCode() dans une classe utilisée dans un HashSet ?

3. Quelle est la méthode recommandée pour supprimer des éléments d'une Collection pendant qu'on la parcourt ?

4. Quel est le but principal de l'utilisation des Génériques (ex: List<Poisson>) depuis Java 5 ?

5. Si je dois constamment insérer et supprimer des éléments au beau milieu de ma liste, quelle implémentation est la plus performante ?

6. Que compare exactement l'opérateur == lorsqu'il est utilisé entre deux objets en Java ?

7. Laquelle de ces affirmations décrit correctement l'interface Map ?


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