Les Collections en Java (List, Set, Map)
Objectif
Comprendre comment manipuler des groupes d’objets en Java grâce aux collections dynamiques.
Cette séance consolide aussi les bases de la POO (attributs, constructeurs, setters/getters).
1. Rappel POO en Java
Une classe définit une structure de données (attributs) et des comportements (méthodes).
Les constructeurs permettent d’initialiser un objet. Les setters assurent la validation.
// src/model/Student.java
package model;
public class Student {
private String id;
private String name;
private int year;
// Constructeur par défaut
public Student() {}
// Constructeur paramétré : validation centralisée via les setters
public Student(String id, String name, int year) {
setId(id);
setName(name);
setYear(year);
}
// Getters
public String getId() { return id; }
public String getName() { return name; }
public int getYear() { return year; }
// Setters avec validation
public void setId(String id) {
if (id == null || id.isBlank()) throw new IllegalArgumentException("id obligatoire");
this.id = id;
}
public void setName(String name) {
if (name == null || name.isBlank()) throw new IllegalArgumentException("nom obligatoire");
this.name = name;
}
public void setYear(int year) {
if (year < 1 || year > 5) throw new IllegalArgumentException("année invalide");
this.year = year;
}
@Override
public String toString() {
return "Student{id='%s', name='%s', year=%d}".formatted(id, name, year);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student s)) return false;
return id.equals(s.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
}
Analyse et explication du code Student.java
Cette classe représente un étudiant avec trois informations principales :
id
: identifiant unique de l’étudiant (chaîne de caractères).name
: nom de l’étudiant.year
: année d’étude (1 = première année, 2 = deuxième, etc.).
1. Les attributs privés
private String id;
private String name;
private int year;
Les attributs sont déclarés private
: cela fait partie du principe d’encapsulation.
On empêche ainsi un autre code d’y accéder directement.
On contrôlera les valeurs grâce à des méthodes get
et set
(getters et setters).
—
2. Le constructeur par défaut
public Student() {}
public Student() {}
Un constructeur est une méthode spéciale appelée lors de la création d’un objet avec new Student()
.
Ce constructeur par défaut ne fait rien, mais il est utile si on veut créer un objet vide puis remplir ses champs plus tard avec les setters.
—
3. Le constructeur paramétré
public Student(String id, String name, int year) {
setId(id);
setName(name);
setYear(year);
}
Ce constructeur initialise directement l’objet avec des valeurs.
Remarque importante : on appelle ici les setters
(et non this.id = id
) pour réutiliser la logique de validation définie dans les setters.
Cela évite de dupliquer le code de contrôle (principe de factorisation).
—
4. Les getters
public String getId() { return id; }
public String getName() { return name; }
public int getYear() { return year; }
Les getters permettent d’accéder à la valeur d’un attribut privé depuis l’extérieur.
Ils ne font qu’un simple retour de la valeur interne.
—
5. Les setters (avec validation)
public void setId(String id) {
if (id == null || id.isBlank())
throw new IllegalArgumentException("id obligatoire");
this.id = id;
}
Ici, on protège l’attribut id
:
- Si la valeur est
null
ou vide (.isBlank()
), on lève une exception. - Sinon, on l’assigne à
this.id
.
L’instruction throw new IllegalArgumentException(...)
crée et lance une exception.
C’est une façon pour la méthode de dire : « je refuse cette valeur, elle est invalide ».
Une exception arrête temporairement l’exécution du programme jusqu’à ce qu’elle soit capturée par un bloc try...catch
(vu plus tard).
Les deux autres setters fonctionnent sur le même principe :
public void setName(String name) {
if (name == null || name.isBlank())
throw new IllegalArgumentException("nom obligatoire");
this.name = name;
}
public void setYear(int year) {
if (year < 1 || year > 5)
throw new IllegalArgumentException("année invalide");
this.year = year;
}
L’intérêt : on évite de créer un étudiant avec des données incohérentes.
C’est une forme de programmation défensive.
—
6. La méthode toString()
@Override
public String toString() {
return "Student{id='%s', name='%s', year=%d}".formatted(id, name, year);
}
Cette méthode renvoie une représentation texte lisible de l’objet, pratique pour l’affichage.
L’annotation @Override
indique que l’on redéfinit une méthode déjà définie dans Object
(la classe mère de toutes les classes Java).
Le mot-clé formatted
(JDK 15+) remplace les anciens String.format()
.
—
7. Les méthodes equals()
et hashCode()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student s)) return false;
return id.equals(s.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
Ces deux méthodes permettent à Java de savoir quand deux objets sont considérés comme égaux.
Ici, deux étudiants sont égaux s’ils ont le même id
.
C’est fondamental pour que les collections comme HashSet
et HashMap
fonctionnent correctement :
equals()
: définit la notion d’égalité logique (même id).hashCode()
: calcule un code numérique dérivé deid
pour accélérer la recherche.
⚠️ Règle d’or :
Si deux objets sont égaux selon equals()
, ils doivent avoir le même hashCode()
.
—
8. Résumé du cycle de vie d’un objet Student
- L’objet est créé via
new Student("S001", "Alice", 2)
. - Le constructeur appelle
setId()
,setName()
,setYear()
. - Les setters vérifient les données et peuvent lever une
IllegalArgumentException
si invalides. - Une fois valide, l’objet est prêt à être ajouté à une collection (List, Set, Map).
- Quand on l’affiche (
System.out.println()
), la méthodetoString()
est utilisée.
—
9. Illustration dans Eclipse
public class TestStudent {
public static void main(String[] args) {
Student s1 = new Student("S001","Alice",2);
System.out.println(s1);
// Exemple d’erreur volontaire
try {
Student s2 = new Student("", "Bob", 3);
} catch (IllegalArgumentException e) {
System.err.println("Erreur : " + e.getMessage());
}
}
}
➡️ Le premier étudiant est créé normalement.
➡️ Le second lève une exception : « id obligatoire ».
Cela montre que la validation fonctionne.
—
10. Bonnes pratiques à retenir
- Toujours valider les données dans les setters (et les appeler dans les constructeurs).
- Toujours redéfinir
equals()
ethashCode()
ensemble. - Utiliser
@Override
pour indiquer clairement une redéfinition. - Lever des exceptions claires en cas de données invalides.
- Fournir un
toString()
utile pour le débogage et l’affichage.
11. Gestion des exceptions avec try / catch
En Java, lorsqu’une méthode « lance » une exception avec throw
,
le programme s’interrompt si cette exception n’est pas capturée.
Pour éviter cela, on utilise un bloc try / catch
pour intercepter l’erreur et réagir.
Exemple simple
public class TestExceptions {
public static void main(String[] args) {
try {
Student s1 = new Student("S001", "Alice", 2);
System.out.println("Créé : " + s1);
// Cas d’erreur : id vide → exception
Student s2 = new Student("", "Bob", 1);
System.out.println("Créé : " + s2);
} catch (IllegalArgumentException e) {
// Ce bloc s’exécute si une exception est levée
System.err.println("Erreur : " + e.getMessage());
}
System.out.println("Programme terminé normalement.");
}
}
Explication :
try { ... }
: contient le code « à risque ».catch (ExceptionType e)
: s’exécute si une exception de ce type est levée.e.getMessage()
: récupère le message d’erreur envoyé parthrow new ...
.
L’instruction System.err.println()
affiche le texte en rouge dans la console, pour signaler une erreur.
—
Version avec finally
Le bloc finally
contient du code exécuté dans tous les cas :
qu’il y ait une erreur ou non (utile pour fermer un fichier, une connexion, etc.).
try {
Student s = new Student(null, "Bob", 2); // id invalide
} catch (IllegalArgumentException e) {
System.err.println("Erreur : " + e.getMessage());
} finally {
System.out.println("Fin du bloc try/catch");
}
—
Exercices pratiques – Validation & Exceptions
Exercice 1 — Validation d’un seul étudiant
Créer un programme TestStudentValidation
qui demande à l’utilisateur (via la console)
de saisir un identifiant, un nom et une année d’étude, puis tente de créer un objet Student
.
Objectif : capturer proprement les erreurs de saisie.
import java.util.Scanner;
import model.Student;
public class TestStudentValidation {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
try {
System.out.print("Id : ");
String id = sc.nextLine();
System.out.print("Nom : ");
String name = sc.nextLine();
System.out.print("Année : ");
int year = Integer.parseInt(sc.nextLine());
Student s = new Student(id, name, year);
System.out.println("Étudiant créé : " + s);
} catch (IllegalArgumentException e) {
System.err.println("Erreur de validation : " + e.getMessage());
} catch (NumberFormatException e) {
System.err.println("Erreur : année non numérique !");
} finally {
sc.close();
}
}
}
💡 Ici, on intercepte deux types d’erreurs :
IllegalArgumentException
: données invalides (levées par les setters),NumberFormatException
: saisie non numérique convertie avecInteger.parseInt()
.
2. Introduction aux collections
List
: ordonnée, indexée, accepte les doublons (ex.ArrayList
).Set
: non ordonnée, pas de doublons (ex.HashSet
).Map
: paires clé→valeur (ex.HashMap
).
Une collection est une structure de données qui permet de regrouper plusieurs objets dans une même variable.
Au lieu de manipuler un seul objet à la fois, une collection sert à stocker, parcourir, trier, rechercher ou modifier un ensemble d’éléments de manière efficace.
Dans la vie courante, on peut comparer une collection à :
- Une liste de noms d’élèves — l’ordre compte, il peut y avoir des doublons.
- Un groupe de personnes uniques — pas de doublons possibles (comme une liste de matricules).
- Un dictionnaire — chaque mot (clé) est associé à une définition (valeur).
En Java, toutes les collections sont regroupées dans le framework Collections du package java.util
.
Elles remplacent les anciens tableaux T[]
beaucoup plus rigides.
—
Pourquoi utiliser des collections ?
Les collections ont plusieurs avantages :
- ✅ Dynamique : on peut ajouter ou retirer des éléments librement (pas de taille fixe comme un tableau).
- ✅ Souple : on peut trier, filtrer, rechercher facilement.
- ✅ Typée : on indique le type d’éléments qu’elle contient, par exemple
List<String>
.
Les trois familles principales de collections sont :
—
1️⃣ Les Listes (List
)
Une List est une collection ordonnée et indexée.
Chaque élément est accessible par sa position (indice).
Elle peut contenir des doublons.
- Implémentation la plus utilisée :
ArrayList
. - Autre implémentation :
LinkedList
(plus efficace pour les insertions/suppressions fréquentes).
import java.util.*;
List<String> noms = new ArrayList<>();
noms.add("Alice");
noms.add("Bob");
noms.add("Chloé");
System.out.println(noms.get(0)); // affiche "Alice"
System.out.println("Taille : " + noms.size());
💡 Ici, noms.get(0)
permet d’accéder au premier élément de la liste, comme dans un tableau.
Mais contrairement à un tableau, on peut y ajouter ou retirer des éléments librement.
—
2️⃣ Les Ensembles (Set
)
Un Set est une collection non ordonnée (l’ordre n’est pas garanti) et qui interdit les doublons.
C’est utile quand on veut s’assurer que chaque élément est unique.
HashSet
: rapide, mais ordre aléatoire.TreeSet
: trie automatiquement les éléments selon leur ordre naturel (ordre alphabétique pour des chaînes, numérique pour des nombres).
Set<String> prenoms = new HashSet<>();
prenoms.add("Alice");
prenoms.add("Bob");
prenoms.add("Alice"); // doublon ignoré
System.out.println(prenoms); // [Alice, Bob]
💡 Ici, même si on ajoute deux fois “Alice”, il n’y a qu’une seule occurrence dans le Set
.
Le test d’unicité se base sur les méthodes equals()
et hashCode()
des objets.
—
3️⃣ Les Dictionnaires (Map
)
Une Map n’est pas vraiment une collection d’objets simples :
c’est une collection de paires clé → valeur.
Chaque clé est unique, et on accède à la valeur correspondante grâce à cette clé.
HashMap
: très rapide, mais ordre non garanti.TreeMap
: trie les clés.LinkedHashMap
: conserve l’ordre d’insertion.
Map<String, Double> notes = new HashMap<>();
notes.put("Alice", 15.5);
notes.put("Bob", 12.0);
notes.put("Chloé", 17.2);
System.out.println("Note de Bob : " + notes.get("Bob"));
notes.forEach((nom, note) -> System.out.println(nom + " → " + note));
💡 Une Map
est idéale pour stocker des données associées :
un identifiant, un nom, un prix, une moyenne, etc.
—
Comparatif rapide
Type | Ordre | Doublons autorisés ? | Clé/Index | Implémentation fréquente |
---|---|---|---|---|
List |
Ordonnée | Oui | Index numérique | ArrayList |
Set |
Non ordonnée | Non | Valeurs uniques | HashSet |
Map |
Non ordonnée | Non (clé unique) | Clé → valeur | HashMap |
—
Exemple d’utilisation combinée
import java.util.*;
public class ExempleCollections {
public static void main(String[] args) {
List<String> villes = new ArrayList<>(List.of("Paris", "Lyon", "Marseille", "Paris"));
Set<String> villesUniques = new HashSet<>(villes);
Map<String, Integer> population = new HashMap<>();
population.put("Paris", 2200000);
population.put("Lyon", 520000);
population.put("Marseille", 870000);
System.out.println("Liste : " + villes);
System.out.println("Set (uniques) : " + villesUniques);
System.out.println("Population de Lyon : " + population.get("Lyon"));
}
}
➡️ Dans cet exemple :
- La
List
contient des doublons (« Paris » deux fois). - Le
Set
garde chaque ville une seule fois. - La
Map
associe chaque ville à sa population.
—
À retenir
- Les collections remplacent les tableaux pour manipuler plusieurs éléments de façon flexible.
- Une
List
garde l’ordre et autorise les doublons. - Un
Set
supprime les doublons. - Une
Map
associe des clés à des valeurs. - Toutes se trouvent dans le package
java.util
.
—
3. Exercices pratiques (Eclipse)
Exercice 1 — Manipulation d’objets dans une List
Créer dans Eclipse un projet Java Collections contenant la classe Student
ci-dessus et une classe TestList
:
// src/app/TestList.java
package app;
import model.Student;
import java.util.*;
public class TestList {
public static void main(String[] args) {
List students = new ArrayList<>();
students.add(new Student("S001","Alice",2));
students.add(new Student("S002","Bob",2));
students.add(new Student("S003","Chloé",1));
// Affichage
for (Student s : students) {
System.out.println(s);
}
// Accès par index
System.out.println("Premier étudiant : " + students.get(0).getName());
}
}
Question : Que se passe-t-il si vous ajoutez deux fois le même étudiant ?
—
Exercice 2 — Dédoublonnage avec Set
Créer une liste d’étudiants contenant des doublons sur l’attribut id
, puis construire un HashSet
à partir de cette liste :
List list = new ArrayList<>();
list.add(new Student("S001","Alice",2));
list.add(new Student("S001","Alice Dup",2));
list.add(new Student("S002","Bob",2));
Set set = new HashSet<>(list);
System.out.println(set);
Questions :
- Combien d’éléments contient le
Set
? - Pourquoi ? (indice :
equals
ethashCode
).
—
Exercice 3 — Map (clé → valeur)
Associer chaque étudiant à sa moyenne via un HashMap
:
Map<String, Double> moyenne = new HashMap<>();
moyenne.put("Alice", 14.5);
moyenne.put("Bob", 12.0);
moyenne.put("Chloé", 16.2);
System.out.println("Moyenne de Bob : " + moyenne.get("Bob"));
Tester ensuite ce code avec un forEach
:
moyenne.forEach((nom, note) ->
System.out.println(nom + " → " + note)
);
—
4. TP final – Gestion d’un catalogue de livres
Objectif
Créer une petite application console qui manipule une liste de livres, supprime les doublons, trie et calcule la moyenne des prix.
Étapes
- Créer la classe
Book
:- Attributs :
isbn:String
,title:String
,price:double
. - Constructeur + setters avec vérification (prix positif, ISBN non vide).
equals()
ethashCode()
basés surisbn
.
- Attributs :
- Créer la classe
AppBook
pour :- a) Créer une
List<Book>
avec des doublons. - b) Dédoublonner avec un
Set
. - c) Indexer les livres par ISBN avec une
Map
. - d) Trier les livres par prix croissant puis titre alphabétique.
- e) Calculer la moyenne des prix.
- a) Créer une
Corrigé (pistes)
books.sort(Comparator.comparingDouble(Book::getPrice)
.thenComparing(Book::getTitle));
Set unique = new HashSet<>(books);
Map<String,Book> byIsbn = new HashMap<>();
unique.forEach(b -> byIsbn.put(b.getIsbn(), b));
double avg = unique.stream().mapToDouble(Book::getPrice).average().orElse(0.0);
System.out.println("Prix moyen : " + avg);
—
5. À retenir
- Toujours redéfinir
equals()
ethashCode()
ensemble. - Un
Set
supprime automatiquement les doublons. - Un
Map
associe une clé unique à une valeur. - Préférer appeler les
setters
dans le constructeur pour centraliser la validation.
Ce contenu est réservé aux membres du site. Si vous êtes un utilisateur existant, veuillez vous connecter. Les nouveaux utilisateurs peuvent s'inscrire ci-dessous.