Collections

Introduction

Les collections en Java permettent de gérer des ensembles d’objets.

Java propose par défaut une bibliothèque de classes permettant de couvrir l’essentiel des besoins d’un programmeur pour gérer des ensembles.

Pour commencer, nous allons étudier la gestion de liste d’objets (List/ArrayList), mais nous verrons plus tard qu’il est possible d’utiliser d’autres types d’ensemble.

Nous allons également découvrir l’utilité des Generics dans l’usage des collections, ainsi que l’Autoboxing/Unboxing.

Les “List”

L’interface List

Un premier type de collection très utilisé est List.

Il permet de traiter un ensemble d’objets sous la forme de liste.

Propriétés

Taille de la liste

Ordre des éléments

Une liste est ordonnée.

Doublons

Le nombre d’objets distinct peut être différent du nombre d’enregistrement.

Méthodes principales

java.util.List est une interface qui propose les fonctions suivantes (non exhaustif).

Taille de la liste

Accès aux éléments

Ajout et suppression d’éléments

Typage des objets contenu

Vous aurez remarqué le type de retour de get(int). E est le type d’objet contenu par la liste. Nous allons voir à quoi cela correspond dans le paragraphe suivant.

Création

L’interface List ne peut être instanciée directement.

java.util.ArrayList est une classe (concrète) qui implémente cette interface et qui peut être instanciée.

Typage des objets et généricité

Une List, par essence, permet de stocker n’importe quel objet. Ce sont donc des instances d’Object qui sont stockés dans une liste.

Il serait peu pratique de devoir spécifier à chaque appel des méthodes get(int), remove(int) le type de l’objet retourné.

Avant les Generics

Avant Java 5.0 (Tiger)

Dans les version antérieures à la version 1.5 (ou 5.0) de Java, il était nécessaire d’écrire ceci:

public class AvantLesGenerics{
  List list = new ArrayList();
  public void init(){
    Date date = new Date(11, 12, 2013);
    list.add(date);
  }

  public void testSiPremiereDateValide(){
    Date d = (Date)list.get(0); //Transtypage indispensable
    System.out.println(d.estValide());
  }
}

Transtypage nécessaire

En l’absence de transtypage, le compilateur indiquait une erreur, car la classe par défaut de List est Object et non Date.

Ce type de programmation était lourd. En effet, à la place de :

Date d = (Date)list.get(0); //Transtypage indispensable

il serait plus simple d’écrire:

Date d = list.get(0); //Utilisation directe de la méthode

Ceci est possible depuis la version 1.5 de Java :

Avec les Generics

Association du type à la liste

Juste après la déclaration du type (List), il est possible d’indiquer le type d’objet qu’elle contient.

Pour cela, il suffit de noter le type entre chevrons < et > :

Cette déclaration indique que la liste contiendra des objets de type Date.

List <Date>.

Diamond operator

Une fois le type déclaré pour l’interface List, il n’est pas utile de le rappeler lors de l’instanciation.

List<Date> list = new ArrayList<>();

Vous noterez que ArrayList est suivi d’un diamant (diamond operator): deux chevrons accolés <>.

Ceci permet au compilateur de savoir que le type générique correspondra à celui de la variable telle que déclarée précédemment

Avant Java 7

Il était obligatoire d’écrire explicitement le type lors de l’appel au constructeur avant la version Java SE 7:

la version du code qui compilait dans ce cas était:

List<Date> list = new ArrayList<Date>();

Ce n’est plus nécessaire depuis la version 7.0.

Exemple d’utilisation

public class AvecLesGenerics{
  List<Date> list = new ArrayList<>();
  public void init(){
    Date date = new Date(11,12,2013);
    list.add(date);
  }

  public void testSiPremiereDateValide(){
    System.out.println(list.get(0).estValide());
  }
}

Nous disposons donc d’un outil puissant et versatile pour stocker des listes d’objets.

Cette notion de Generics reste à approfondir mais ce que nous venons de voir va nous suffire dans un premier temps.

Autoboxing / unboxing

Stockage des types primitifs

Exemple

L’intérêt des collections est qu’elles permettent également de gérer des types primitifs.

En effet, même si ces types ne sont pas des instances, ils vont pouvoir être encapsulés dans des objets.

Ainsi, on aura la possibilité de stocker un int dans un objet de type Integer.

Observez bien le code suivant:

public class IntBoxing {
    List<Integer> listeInt = new ArrayList<>();
    public void init(){
        int monNombrePrimitif = 24;
        listeInt.add(monNombrePrimitif);
    }

    public void faire(){
        System.out.println(listeInt.get(0) + 12);
    }
    public static void main(String[] args) {
        IntBoxing ib = new IntBoxing();
        ib.init();
        ib.faire();
    }
}

Il est parfaitement fonctionnel.

Un type = une contenant

Il vous est possible d’utiliser les collections pour stocker les types primitifs suivants en utilisant les réceptacles associés :

 Type primitif Classe réceptacle
boolean Boolean 
byte Byte  
char Character
float Float
int Integer
long Long
short Short
double Double

Ces réceptacles sont des objets boîtes (box) dans lesquelles sont stockées les valeurs primitives afin qu’elles soient prises en charge par les collections comme des objets.

Boucles for(;;) et for-each

Deux formes d’itérations

Il est possible de parcourir une liste de deux manières avec une boucle for:

Interface List et classes associées

Nous avons utilisé l’interface List avec la classe ArrayList. Mais il est possible de l’interchanger avec une autre classe sans effet sur le reste du code.

Par exemple avec la classe LinkedList:

public class IntBoxing {
    List<Integer> listeInt = new LinkedList<>();
    public void init(){
        int monNombrePrimitif = 24;
        listeInt.add(monNombrePrimitif);
    }
  //... (le reste du code est identique à l'exemple précédent
}

La classe LinkedList est une liste chaînée. Selon le cas, son mode de fonctionnement sera plus performant que si on utilise une ArrayList.

Vous pouvez tester cela avec le code suivant:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class PerformancesList {

    public static final int MAX_VALUE = 100000;

    List<Integer> arrayList = new ArrayList<>();
    List<Integer> linkedList = new LinkedList<>();

    public void insert(List<Integer> liste, int maxValue) {
        for (int i = 0; i < maxValue; i++) {
            liste.add(0, i);
        }
    }

    public void parcours(List<Integer> liste, int maxValue) {
        for (int i = 0; i < maxValue; i++) {
            Integer integer = liste.get(i);
        }
    }

    public static void main(String[] args) {
        PerformancesList pl = new PerformancesList();
        pl.insert(pl.arrayList, 10);
        pl.insert(pl.linkedList, 10);

        System.out.println("-- Insertion");
        long debutOp = System.nanoTime();
        pl.insert(pl.linkedList, MAX_VALUE);
        long finOp = System.nanoTime();
        System.out.println("Linked :" + (finOp - debutOp) / 1000000 + "ms");

        debutOp = System.nanoTime();
        pl.insert(pl.arrayList, MAX_VALUE);
        finOp = System.nanoTime();
        System.out.println("Array :" + (finOp - debutOp) / 1000000 + "ms");

        pl.parcours(pl.arrayList, 10);
        pl.parcours(pl.linkedList, 10);

        System.out.println("-- Parcours");
        debutOp = System.nanoTime();
        pl.parcours(pl.linkedList, MAX_VALUE);
        finOp = System.nanoTime();
        System.out.println("Linked :" + (finOp - debutOp) / 1000000 + "ms");

        debutOp = System.nanoTime();
        pl.parcours(pl.arrayList, MAX_VALUE);
        finOp = System.nanoTime();
        System.out.println("Array :" + (finOp - debutOp) / 1000000 + "ms");

    }
}

Que constatez-vous à l’exécution de ce code ?

Exercices

Parcours de liste

Remplissez une liste avec des entiers numérotés de 1 à 10.

Utilisez les deux types de boucle for pour parcourir une List d’entiers (de 1 à 20).

Vue d’ensemble des Collections

Les classes Collection

Le diagramme suivant donne une vision globale des Collections en Java

Collections
Collections

On s’aperçoit que List est un sous-type de Collection, tout comme Set ou Queue.

Nous allons ici nous intéresser sur certaines fonctionnalités de base de ces classes Set et Queue.

Vous pouvez trouver une documentation plus complète sur les collections ici

Set

Définition

L’interface Set permet de contenir un ensemble d’objets. Cet ensemble ne contient pas de doublon (un objet y est stocké une et une seule fois).

Exemple

package com.example.collections;

import java.util.Set;
import java.util.TreeSet;

public class ExempleSet {
    public static void main(String[] args) {
        Set<Integer> entiers = new TreeSet<>();
        entiers.add(1);
        entiers.add(2);
        entiers.add(3);
        entiers.add(2);
        entiers.add(3);
        entiers.add(4);
        entiers.remove(3);
        System.out.println(entiers);
    }
}

Sous-types

Queue

Usage

Les sous-classes de l’interface Queue permettent de gérer des files d’attentes (de type FIFO ou LIFO).

Méthodes

Deux façons d’utiliser un objet de type Queue

Type d’opération Lance une exception Renvoie une valeur spéciale
Insert add(elem) offer(elem) 
Remove remove() poll() 
Examine element() peek() 

Exemple

package com.example.collections;

import java.util.LinkedList;
import java.util.Queue;

public class ExempleQueue {

    public static void main(String[] args) {
        int nombreClients = 0 ;
        Queue<String> queue = new LinkedList<>();

        while (nombreClients < 10){
            queue.add("Client("+nombreClients+")");
            nombreClients++;
        }

        while (!queue.isEmpty()) {
            System.out.println(queue.remove());
        }
    }
}

L’interface Map et ses sous-classes

Association clef-valeur

Une Map permet de stocker des éléments en les référençant via une clef d’accès.

C’est l’équivalent d’un dictionnaire. Pour trouver la définition d’un mot, on cherche le mot en question. Une fois trouvé, il est possible d’en lire la définition.

L’utilisation des Map permet de stocker et récupérer facilement une information, un pointeur ou un objet associé à un autre objet.

Types des clefs et des valeurs

Une Map va associer

Exemple

import java.util.Map;
import java.util.HashMap;

public class Anniversaire {
    private Map<String, Personne> anniv = new HashMap<>();
    public void init(){
        anniv.put("19/05/1955", new Personne("Gosling", "James"));
        anniv.put("09/09/1941", new Personne("Ritchie", "Dennis"));
        anniv.put("04/02/1943", new Personne("Thompson", "Ken"));
    }

    public void affichePrenom(String date){
        System.out.println(anniv.get(date).prenom);
    }

    public static void main(String[] args) {
        Anniversaire a = new Anniversaire();
        a.init();
        a.affichePrenom("04/02/1943");
    }
}

class Personne{
    String nom;
    String prenom;
    Personne(String nom, String prenom){
        this.nom = nom;
        this.prenom = prenom;
    }
}

Classes de type Map

On peut utiliser à la place de HashMap un type TreeMap ou EnumMap ainsi que d’autres classes (voir la Javadoc correspondante)

Les différences d’implémentation entre ces différentes classes ne seront pas discutées ici.

Parcours

Interface Iterator

Parmi les fonctionnalités communes aux collections, il y a la possibilité de parcourir la liste des éléments d’une collection via l’interface Iterator.

Cas particulier: Map

Map est ici un peu à part. Elle est liée aux collections dans le fait qu’elle possède deux méthodes qui renvoient des instances de l’interface Set:

À partir de ces instances de Set il est possible de récupérer un Iterator pour en parcourir les éléments de la Map.

Iterator

Exemple de parcours

Voici un exemple d’utilisation d’un Iterator. Ceci est applicable à toute sous-interface de Collection:


public void parcourir(Collection collection) {
   Iterator it = collection.iterator();

   while(it.hasNext()) {
      System.out.println(it.next());
   }
 }

Parcours avec for et for-each

package com.example.collections;

import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

public class ExempleIteration {
    public static void main(String[] args) {
        Set<Integer> entiers = new TreeSet<>();
        entiers.add(1);
        entiers.add(2);
        entiers.add(3);
        entiers.add(4);
        entiers.remove(3);
        entiers.add(16);
        entiers.add(8);

        System.out.println("For sur Iterator");
        for (Iterator<Integer> iterator = entiers.iterator(); iterator.hasNext();) {
            Integer entier = iterator.next();
            System.out.println(entier);
        }

        System.out.println("For-each");
        for (Integer entier : entiers) {
            System.out.println(entier);
        }
    }
}

Application

List : création de listes de véhicules

Nous allons cette fois gérer des listes de Vehicule (en créant une nouvelle classe Vehicule dans le paquet com.example.gestionflotte).

Cette classe comportera les attributs permettants de stocker les informations suivantes:

D’un point de vue administratif, une personne (physique ou morale) est propriétaire d’un ou plusieurs véhicules. Ces véhicules peuvent être des types suivants (le nombre de type est ici limité pour simplifier le programme) :

Une entreprise doit gérer une flotte de véhicule ayant ces différents types.

Pour chacun des points demandés, tester le code à partir la méthode main de la classe GestionFlotte créée plus bas.

Il n’est pas demandé de gérer l’ajout ou l’affichage par des commandes au clavier.

Le test du programme se fera en ajoutant “en dur” différents véhicules et en examinant la sortie (console) qu’il fournit.

Le programme doit s’exécuter entièrement automatiquement et afficher des messages sur chaque instruction en cours d’exécution.

Création des types de véhicules

  1. Créer la classe Vehicule et faite en sorte qu’un identifiant unique soit associé à chaque nouvelle instance.
  2. Créer une classe pour chaque type (VehiculeLeger, VehiculeLourd, VehiculeTransportPassagers)
  3. Pour la classe VehiculeTransportPassagers, ajouter un attribut indiquant le nombre maximal de passagers nommé nombreMaxPassagers
  4. Pour la classe VehiculeLeger, indiquer le type: entreprise (voiture à deux places), utilitaire ou particulier (véhicule à 5 places).
  5. Redéfinir la méthode equals qui permet de vérifier si deux véhicules sont identiques (même marque, même modèle). Ils peuvent avoir des identifiants différents.
  6. Implémenter la méthode toString de la classe véhicule pour afficher l’identifiant, la marque et le modèle sous cette forme: "[134,marque,modèle]"

On souhaite pouvoir gérer les listes suivantes:

La classe GestionFlotte va comporter une méthode main.

  1. Cans la classe GestionFlotte, ajouter ces trois listes. Il ne doit pas être possible d’ajouter un type incorrect dans les différentes listes (pas de Véhicule Léger dans la liste des véhicules de transport de passagers).
  2. Ajouter une méthode permettant d’ajouter un véhicule quelconque dans la liste globale: ajouterVehicule(Vehicule v)
  3. Ajouter une méthode permettant de supprimer un véhicule de la liste globale: supprimerVehicule(Vehicule v)
  4. Ajouter une méthode indiquant le nombre de véhicules dans la liste globale : getNombreVehicules()
  5. Tester le code en ajoutant puis supprimant plusieurs véhicules et en vérifiant si les valeur renvoyées sont cohérentes

List: affichage d’une liste de véhicules

Dans cette partie, il s’agit d’afficher les listes de véhicules sous forme de chaînes de caractères.

  1. Créer une méthode permettant de récupérer sous forme de chaîne de caractère l’ensemble des véhicules pour chaque type (trois méthodes à créer):
- `getListeInformationsTousVehicules()`
- `getListeInformationsVehiculesLegers()`
- `getListeInformationsVehiculesTransportsPassagers()`
  1. Ajouter une méthode permettant de remplir automatiquement les deux listes spécialisées à partir de la liste globale. Un élément déjà présent dans une liste ne doit pas être ajouté.
- `synchroniserListesSpecialisees()`
  1. Faire en sorte que tout ajout de véhicule dans la flotte complète automatiquement les listes spécialisées si elles sont concernées (à chaque appel de la méthode ajouterVehicule(Vehicule v)).
  2. Tester avec au moins deux instances pour chaque type de véhicule.

(Queue et Set)

Reprendre les classes permettant la gestion d’une flotte de véhicules (GestionFlotte).

  1. Ajouter à la classe GestionFlotte un attribut marquesVehicules de type Set donnant l’ensemble des marques de véhicules gérés par GestionFlotte.
```java

Set<String> marquesVehicules = new HashSet<>();

```
  1. Créer une méthode void syncMarquesVehicules() permettant de remplir cet ensemble à partir de la liste de tous les véhicules (listeGlobale).
  2. Créer une méthode String getListeMarquesVehicules() qui renvoie une chaîne de caractères représentant les données contenues dans marquesVehicules (exemple: "Mercedes,Toyota,Peugeot")
  3. Ajouter à la classe GestionFlotte un attribut modelesVehicule de type Set qui permet de gérer l’ensemble des marques et modèles de véhicule (un modèle de véhicule pouvant être présent plusieurs fois dans listeGlobale).
  4. Créer une méthode syncModelesVehicules qui permet de remplir automatiquement modelesVehicule (lire la documentation de Set.addAll(Collection) et souvenez-vous de l’implémentation de la méthode equals)
  5. Créer une méthode String getListeModelesVehicules() qui renvoie une chaîne de caractères représentant les données contenues dans modelesVehicule (exemple: "[Mercedes/Classe A],[Toyota/Prius],[Toyota/Yaris],[Peugeot/Partner]")

Map : référencement de véhicule

Dans ce qui suit, vous traiterez les cas d’erreurs avec des exceptions (pour lesquelles vous créerez des classes).

Dans cette partie, nous allons références les véhicules par leur identifiant.

  1. Créer une Map permettant de stocker la liste des véhicules en les référençant par leur identifiant.

Iterator: application

Utilisez un Iterator pour afficher l’ensemble des éléments de la liste des véhicules légers.