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
-
Créez une classe abstraite
EntiteAquatiqueavec :- Des attributs protégés :
nom(String),xety(int), etvitesse(int). - Un constructeur pour initialiser tout cela.
- Important pour la fenêtre : Ajoutez les méthodes publiques
getX(),getY()etgetNom(). - Déclarez une méthode abstraite
public abstract void seDeplacer();.
- Des attributs protégés :
-
Créez une classe
Poissonhéritant deEntiteAquatique.- Son déplacement : il avance en
xde1 * vitesse. Sixdépasse 800 (le bord de l'écran), remettezxà 0.
- Son déplacement : il avance en
-
Créez une classe
SousMarinhéritant deEntiteAquatique.- Son déplacement : il avance en
xde1 * vitesseet oscille de bas en haut (modifiez légèrement ley).
- Son déplacement : il avance en
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
}
}
: 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 :
Exercice 2 : L'Aquarium Dynamique
- Dans votre
Main, remplacez le tableau par uneList<EntiteAquatique>. - Utilisez la méthode
.add()pour insérer vos poissons. - 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.
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 (deuxnew). - La méthode
equals(), héritée deObject, compare le contenu. Mais par défaut, elle se contente de faire un==!
Exercice 4 : Redéfinir l'égalité
- Dans votre classe
Poisson, redéfinissez (Override) la méthodeequals(). - Faites en sorte que deux poissons soient considérés égaux s'ils ont le même nom.
- Relancez l'exercice 3 :
p1.equals(p2)doit maintenant renvoyertrue!
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 :
- Pour bien voir les deux poissons à l'écran, ajoutez une méthode
public void setY(int y)dans la classeEntiteAquatique. - Décalez le deuxième poisson vers le bas :
p2.setY(80);. - Créez un
Set<EntiteAquatique> especes = new HashSet<>();. - Ajoutez-y vos poissons
p1etp2. - 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
- Dans
Poisson, générez/redéfinissez la méthodehashCode()en vous basant uniquement sur l'attributnom. - 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é
- Retournez dans votre classe
Poisson. - Modifiez
equals()pour qu'il compare tous les attributs (nom,x,y,vitesse). - Modifiez
hashCode()en utilisantObjects.hash(nom, x, y, vitesse). - 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 !
- Créez une
Map<String, Iterable<EntiteAquatique>> parc = new HashMap<>();. - Créez deux listes (ou Sets) : de Poissons, et de Sous-Marins.
- Ajoutez-les dans la Map (clés :
"Bassin Tropical","Zone Abyssale"). - 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
- Dans le
Main, créez une listeArrayList<EntiteAquatique>avec beaucoup de poissons, certains rapides (vitesse > 3) et d'autres lents. - Créez une méthode statique
attaquerBassin(List<EntiteAquatique> bassin). - 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()dansEntiteAquatique), supprimez-la de la liste avecbassin.remove(entite). - 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 :
Correction du Bonus
- Remplacez la boucle
for-eachpar une boucleforouwhileavec un Iterator et utilisez la méthoderemove()de ce dernier. - Relancez l'aquarium : seuls les poissons les plus rapides ont survécu !
- Effacez finalement votre boucle et remplacez-la par un appel à
removeIfen 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
- Dans le
Main, créez une listeArrayList<EntiteAquatique>avec beaucoup de poissons, certains rapides (vitesse > 3) et d'autres lents. - Créez une méthode statique
attaquerBassin(List<EntiteAquatique> bassin). - 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). - 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 :
- À l'ancienne (avant Java 8) : l'Iterator. Utilisez un
Iterator<EntiteAquatique> it = bassin.iterator();avec une bouclewhile(it.hasNext()). Pour supprimer, appelezit.remove()(c'est l'itérateur qui supprime de manière sécurisée, pas la liste directement !). - La méthode moderne (Java 8+) :
Utilisez la méthode
removeIfpré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)