Seb-Info

Java (partie 2)

Java (partie 2)

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() {}

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é de id 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

  1. L’objet est créé via new Student("S001", "Alice", 2).
  2. Le constructeur appelle setId(), setName(), setYear().
  3. Les setters vérifient les données et peuvent lever une IllegalArgumentException si invalides.
  4. Une fois valide, l’objet est prêt à être ajouté à une collection (List, Set, Map).
  5. Quand on l’affiche (System.out.println()), la méthode toString() 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() et hashCode() 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é par throw 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 avec Integer.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 et hashCode).

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

  1. Créer la classe Book :
    • Attributs : isbn:String, title:String, price:double.
    • Constructeur + setters avec vérification (prix positif, ISBN non vide).
    • equals() et hashCode() basés sur isbn.
  2. 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.
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() et hashCode() 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.

Connexion pour les utilisateurs enregistrés
   
Nouvel utilisateur ?
*Champ requis
Powered by WP-Members