Aller au contenu

TP3 - Les collections⚓︎

Dans ce sujet de TP, le thème abordé est l'utilisation et la compréhension des classes du JDK permettant de modéliser des regroupements d'objets (des collections).

1. Présentation⚓︎

L'API (interface de programmation) Collection est une des API les plus utilisées en Java. Elle a été introduite dans la version 1.2 de Java, et entièrement réécrite dans la version 1.5 pour gérer les génériques. Cette API est présente dans le package java.util.

Cette API a pour ojectif de permettre la gestion de collections d'objets, c'est-à-dire de regroupements d'objets. En effet, les tableaux ne permettent pas de répondre à tous les besoins de stockage et surtout ils manquent de fonctionnalités.

Cette API propose donc plusieurs interfaces (et plusieurs implémentations) afin de répondre aux différents besoins que nous pouvons avoir : notre collection peut être contenir des doublons ? doit-on pouvoir la trier ? ...

2. Interfaces principales⚓︎

Cette API contient principalement deux familles de regroupements d'objets : java.util.Collection et java.util.Map. Pour simplifier, une Collection gère un regroupement des objets en question directement, alors qu'une Map gère un regroupement de couple (clef, valeur). Les valeurs sont les objets en question, qui sont donc identifiés par une clef.
L'interface Collection contient elle-même principalement deux sous-familles : java.util.List et java.util.Set.

API collection, collections et java.util.Collection

Attention, on utilise ici le même mot, "collection", pour trois choses différentes :

  1. Le nom de cette API : "Collection".
  2. Des regroupements d'objets : ces sont des "collections".
  3. Une partie de ce package (la branche de gauche dans schéma ci-dessous) : l'interface java.util.Collection.

Nous parlerons donc ici des interfaces List, Set et Map.

Voici un diagramme simplifié des principales interfaces proposées par cette API :

graph BT;
    D[java.util.List] -.-> B[java.util.Collection];
    F[java.util.SortedSet] -.-> E[java.util.Set];
    E[java.util.Set] -.-> B[java.util.Collection];
    G[java.util.SortedMap] -.-> C[java.util.Map];
    B[java.util.Collection] -.-> A([API Collection]);
    C[java.util.Map] -.-> A([API Collection]);

Diagramme de java.util.Collection

Voici un diagramme plus détaillé des interfaces et classes proposées par cette API (dans la branche de gauche du diagramme ci-dessus : java.util.Collection).

Diagramme de java.util.collection

3. Utilisation de ces interfaces⚓︎

Chacune de ces interfaces propose plusieurs classes les implémentant, mais voici les plus "classiques", avec une explication sur quand les utiliser :

Interface Implémentation "classique" Éléments ordonnés Accès direct à un élément Clé / valeur Doublons autorisés ? Correspondance en Python
List java.util.ArrayList Oui Oui Non Oui list : [1, 2, 3]
Set java.util.HashSet Non Non Non Non set : {1, 2, 3}
SortedSet java.util.TreeSet Oui Non Non Non N/A
Map java.util.HashMap Non Oui Oui Non 1 dict : {1: "un", 2: "deux"}
SortedMap java.util.TreeMap Oui Oui Oui Non 2 N/A
  • Éléments ordonnés : cela signifie que les éléments sont par défaut dans l'ordre dans lequel ils ont été ajoutés. La collection est de plus triable (cf. ci-dessous Comparable et Comparator).
  • Accès direct à un élément : cela signifie qu'on peut accéder directement à un élément, sans avoir à parcourir toute la collection.
  • Clé / valeur : cela signifie que chaque élément de la collection est un couple (clé, valeur). La clé peut être n'importe quel objet java (java.lang.Object), mais il est préférable (et fortement conseillé) que les méthodes equals() et hashCode() soient surchargées. La valeur peut également être n'importe quoi.
  • Doublons autorisés ? : est-il possible d'avoir deux fois le même objet dans la collection ?

4. Contenu supplémentaire⚓︎

Cette API fournit également :

  • deux interfaces pour le parcours de certaines collections : java.util.Iterator et java.util.ListIterator.
  • deux interfaces pour permettre le tri de certaines collections : java.lang.Comparable et java.util.Comparator (exemple ici) :
    • Comparable permet d'indiquer comment des objets doivent être triés.
    • On souhaite parfois pouvoir trier une collection d'objets sur différentes propriétés. On souhaite par exemple à un moment trier une liste de personne par rapport à leur nom, puis ensuite par rapport à leur prénom, puis par rapport à leur âge, puis par rapport à leur profession ... On utilise alors autant de Comparator que souhaité.
  • des classes utilitaires : java.util.Arrays, java.util.Collections.

5. Exercices⚓︎

Avant de commencer les exercices, dans Eclipse, créer un projet Java nommé TPCollections.

Exercice 1 - La structure Notes
  1. Écrire une structure de données permettant de représenter un ensemble de notes, d'y ajouter des notes et de calculer la moyenne de celles-ci. Vous utiliserez la classe ArrayList pour représenter cet ensemble.

    Voici le diagramme de cette classe fr.univtours.polytech.tpcollections.Notes :

    classDiagram
        Notes
        class Notes{
            -notesList: List~Double~
            +Notes()
            +getNotesList() List~Double~
            +addNote(note: Double)
            +computeMean() Double
        }

    Note

    Depuis la version 1.5 de Java, on utilise des génériques. Cela signifie que lorsqu'on définit une liste par exemple, on indique quel est le type d'objet qu'elle contient. En indiquant private List<Double> notesList, on précise que cette liste ne contient que des Double, ou des objets dont la classe hérite de Double.

    Note

    On utilisera donc la classe java.util.ArrayList comme implémentation de l'interface java.util.List.

    Cela signifie qu'on peut écrire des choses comme List<Double> maList = new ArrayList<Double>();.

    • À gauche du =, on définit l'objet : on indique que c'est une List, c'est-à-dire qu'on indique les fonctionnalités que notre objet doit avoir.
    • À droite du =, on précise l'implémentation qu'on a choisit : on indique que c'est une ArrayList, c'est-à-dire qu'on précise comment les fonctionnalités que notre objet doit avoir sont implémentées.
  2. Cette classe possède un constructeur public sans argument. Son rôle est d'initialiser chacuns des attributs de la classe. Dans le cas présent, ce constructeur ressemblera à cela :

    ☕ Code Java
    public Notes() {
        this.notesList = new ArrayList<Double>();
    }
    
  3. Écrire une classe fr.univtours.polytech.tpcollections.TestNotes, contenant une méthode main permettant de tester cette classe Notes.

    Pour cela, initialiser un objet Notes, lui ajouter les notes 10, 12 et 20, et vérifier que l'exécution de la méthode computeMean renvoie bien 14 (ou plutôt 14.0).

    Les Double

    En Java, en ajoutant D après un nombre à virgule, on obtient directement un Double. Il est donc possible d'écrire :

    ☕ Code Java
    notes.addNote(18D);//(1)!
    
    1. On ajoute la note 18 dans l'objet Notes.
Exercice 2 - La structure Étudiant
  1. Écrire une structure de données (une classe) permettant de représenter une matière, chaque matière aura un nom et un coefficient.

    Voici le diagramme de cette classe fr.univtours.polytech.tpcollections.Subject :

    classDiagram
        Subject
        class Subject{
            -name: String
            -coefficient: Double
            +Subject(name: String, coefficient: Double)
            +getName() String
            +setName(name: String)
            +getCoefficient() Double
            +setCoefficient(coefficient: Double)
        }

  2. Compléter la classe fr.univtours.polytech.tpcollections.TestNotes pour tester cette nouvelle classe.

    Pour cela, on créera deux matières :

    • Informatique, avec un coefficient de 1,5.
    • Mathématiques, avec un coefficient de 2,5.
  3. Écrire une structure de données (une classe) permettant de représenter un étudiant. Chaque étudiant possèdera un nom et une liste de notes par matière.

    Il sera en outre possible de calculer sa moyenne pondérée (moyenne en prenant en compte le coefficient de chaque matière).

    Voici le diagramme de cette classe fr.univtours.polytech.tpcollections.Student :

    classDiagram
        Student
        class Student{
            -name: String
            -notesList: Map~Subject, Notes~
            +Student(name: String)
            +getName() String
            +setName(name: String)
            +getnotesList() Map
            +computeMean() Double
        }
    Vous utilisez la classe java.util.HashMap comme implémentation de l'interface java.util.Map.

    À nouveau, le constructeur public à uniquement comme rôle d'initiliser cette Map :

    ☕ Code Java
    public Student(String name) {
        this.name = name;
        this.notesList = new HashMap<Subject, Notes>();
    }
    
    Les Map en Java

    Pour rappel, pour parcourir un dictionnaire en Python, on fait

    🐍 Script Python
    for clef in dictionnaire:
        dictionnaire[clef] # permet d'obtenir la valeur associée à clef.
    

    En Java, l'idée est là même :

    • La méthode Map.keySet() permet d'obtenir la liste (en fait l'ensemble) des clefs.
    • La méthode Map.get(Object key) permet d'obtenir la valeur associée à la clef.

    Cela donne donc (dans notre exemple, la clef est un objet Subject et la valeur associée un objet Notes) :

    ☕ Code Java
    for (Subject matiere : this.notesList.keySet()) {
        this.notesList.get(matiere); // permet d'obtenir la valeur associée à la clef.
    }
    
  4. Compléter la classe fr.univtours.polytech.tpcollections.TestNotes pour tester cette nouvelle classe. On créera une étudiante : "Alice".

    Alice aura :

    • Les trois notes créées dans l'exercice 1 (10, 12 et 20) en informatique;
    • 18 et 20 en mathématiques.

    Vérifier qu'Alice a bien 17,125 de moyenne.

    Comment ajouter des notes à Alice en maths ?

    ... ou de manière générale, comment ajouter des couples (clef, valeurs) dans une Map ?

    De la même manière que Map.get(Object key) permet d'accéder à la valeur associée à la clef key, la méthode Map.put(Object key, Object value) permet d'ajouter un nouveau couple dans la Map.

    On souhaite par exemple indiquer qu'Alice a eu 18 et 20 en maths.

    ☕ Code Java
    // Création de la matière.
    Subject maths = new Subject("Mathématiques", 2.5D);
    // Création de l'étudiante.
    Student alice = new Student("Alice");
    // Création de ses notes :
    Notes aliceEnMaths = new Notes();
    aliceEnMaths.addNote(18D);
    aliceEnMaths.addNote(20D);
    // On récupère l'objet contenant toute les notes d'Alice : alice.getNotesList() ...
    // ... auquel on ajoute le couple choisi : .put(...)
    alice.getNotesList().put(maths, aliceEnMaths);
    
Exercice 3 - La structure Promotion
  1. Écrire une structure de données permettant de représenter une promotion qui est une liste d'étudiant. Une promotion étudie dans une année précise avec une liste de matières.

    Il doit être possible d'obtenir :

    • la moyenne générale de la promotion,
    • la moyenne par matière,
    • le classement de la promotion,
    • les meilleures et moins bonne notes dans chaque matière.

    Voici donc le diagramme de cette classe fr.univtours.polytech.tpcollections.YearGroup :

    classDiagram
        YearGroup
        class YearGroup{
            -year: Integer
            -students: List~Student~
            -subjects: List~Subject~
            +YearGroup()
            +getYear() Integer
            +setYear(year: Integer)
            +getStudents() List~Student~
            +setStudents(students: List~Student~>)
            +getSubjects() List~Subject~
            +setSubjects(subjects: List~Subject~)
            +computeGroupMean() Double
            +computeSubjectMean(subject: Subject) Double
            +getGroupRanking() List~Student~
            +getBestNote(subject: Subject) Double
            +getWorseNote(subject: Subject) Double
        }

    Trier des collections

    Vous trouverez un objet sur l'utilisation de l'interface java.util.Comparator ici : https://stackoverflow.com/questions/2839137/how-to-use-comparator-in-java-to-sort.

    On pensera à utiliser des "expressions lambda" (cf. dernière partie de la réponse dans le post ci-dessus, après le EDIT).

  2. Créer la classe fr.univtours.polytech.tpcollections.TestNotesFinal, afin de vérifier le bon fonctionnement de notre modèle. Il suffit de copier le code suivant :

    ☕ Code Java
    public static void main(String[] args) {
        // Création de deux matières :
        Subject info = new Subject("Informatique", 1D);
        Subject maths = new Subject("Mathématiques", 2D);
    
        // Création de 3 étudiants :
        Student alice = new Student("Alice");
        Student bob = new Student("Bob");
        Student charlie = new Student("Charlie");
    
        List<Student> students = new ArrayList<Student>();
        students.add(alice);
        students.add(bob);
        students.add(charlie);
    
        // Création de la promotion :
        YearGroup yearGroup = new YearGroup();
        yearGroup.setYear(2022);
        yearGroup.setStudents(students);
    
        // Alice a 14 en info et 20 en maths.
        Notes aliceEnInfo = new Notes();
        aliceEnInfo.addNote(14D);
        alice.getNotesList().put(info, aliceEnInfo);
        Notes aliceEnMaths = new Notes();
        aliceEnMaths.addNote(20D);
        alice.getNotesList().put(maths, aliceEnMaths);
    
        // Bob a 16 en info et 10 en maths.
        Notes bobEnInfo = new Notes();
        bobEnInfo.addNote(16D);
        bob.getNotesList().put(info, bobEnInfo);
        Notes bobEnMaths = new Notes();
        bobEnMaths.addNote(10D);
        bob.getNotesList().put(maths, bobEnMaths);
    
        // Charlie a 15 en info et 15 en maths.
        Notes charlieEnInfo = new Notes();
        charlieEnInfo.addNote(15D);
        charlie.getNotesList().put(info, charlieEnInfo);
        Notes charlieEnMaths = new Notes();
        charlieEnMaths.addNote(15D);
        charlie.getNotesList().put(maths, charlieEnMaths);
    
        System.out.println("Classement :");
        System.out.println(yearGroup.getGroupRanking());
    
        System.out.println("Moyenne générale :");
        System.out.println(yearGroup.computeGroupMean());
        System.out.println("Moyenne en maths :");
        System.out.println(yearGroup.computeSubjectMean(maths));
    
        System.out.println("Meilleure note en maths :");
        System.out.println(yearGroup.getBestNote(maths));
        System.out.println("Moins bonne note en maths :");
        System.out.println(yearGroup.getWorseNote(maths));
    }
    
  3. Dans la classe Student, surcharger la méthode toString() pour qu'elle affiche le nom de l'étudiant.

  4. Vérifier que l'exécution de cette classe renvoie le résultat suivant :

    Console
    Classement :
    [Alice, Charlie, Bob]
    Moyenne générale :
    15.0
    Moyenne en maths :
    15.0
    Meilleure note en maths :
    20.0
    Moins bonne note en maths :
    10.0
    

  1. Il n'est pas possible d'avoir deux fois la même clef. Il est en revanche possible d'avoir deux fois le même objet, associé à deux clefs différentes. 

  2. Idem que 1