Collections du JDK

L’API de collection est séparée en 2 types de fonctionnalités : les collections à proprement parler avec les interfaces Collection, List, Set et leurs sous interfaces, et les maps. Les bonnes pratiques imposent de déclarer les variables avec une interface et de n’utiliser les classes que pour l’instanciation.

Classes et interfaces

Les couples classe / interface classiques sont les suivants :

 Collection<String> col = new ArrayList<>();
 List<String> list = new ArrayList<>();
 Set<String> set = new HashSet<>();
 Map<String, String> map = new HashMap<>();

L’utilisation de classes anciennes, comme Vector ou Hashtable, est généralement à proscrire.

Avec le JDK 11, la façon la plus simple d’instancier une collection est d’utiliser les nouvelles méthodes de fabrique. Avec ces méthodes, on ne se préoccupe plus du type concret de la collection obtenu, il faut juste savoir qu’elle n’est pas modifiable.

 List<String> list = List.of("AZERTY", "QSDFGH", "WXCVBN");

Et ça fonctionne aussi pour créer une Map.

 Map<String, String> map = Map.of(
                                "A", "AZERTY",
                                "Q", "QSDFGH",
                                "W", "WXCVBN"
                              );

Boucles for each

Depuis le JDK 5, une nouvelle forme de boucle for, surnommée « for each » a été introduite. Elle est beaucoup plus pratique que l’ancienne forme, mais présente des contraintes lorsqu’on veut manipuler la collection qu’on parcourt car l’itérateur n’est pas accessible.

 for (Type var : col) {
   col.remove(var);   // Ceci est interdit !!!
 }

Le problème rencontré est une ConcurrentAccessException lorsqu’on supprime un élément de la collection en cours d’itération. L’ancienne boucle, du fait qu’elle sépare distinctement la collection de son itérateur, permet d’ajouter ou de retirer depuis le corps de la boucle de éléments de la collection. Ceci fonctionne à condition de la faire via l’itérateur et non directement sur la collection.

 for (Iterator<Type> it = col.iterator();it.hasNext();) {
   Type var =  it.next();
   ...
   col.remove(var);   // Ceci est toujours interdit
   it.remove();       // Ceci est autorisé
   ...
 }

Tri

Le tri d’une liste se fait en passant la liste à trier à la méthode java.util.Collections.sort(list). Cette méthode trie les objets de la liste selon le résultat de leurs méthodes compareTo(). Cela signifie donc que ces objets doivent implémenter l’interface Comparable.

Collections.sort(list);

Pour trier des listes d’objets qui n’implémente pas cette interface, on peut choisir d’utiliser un Comparator et en passer une instance à la méthode java.util.Collections.sort(list, comparator).

Stream et lambda

Avec le JDK 8, l’introduction de l’API Stream a changé par mal de choses dans la manipiulation des collections.

newCol = col.stream()
            .filter(...)
            .sort()
            .toList();

Pour les comparateurs, la notation lambda simplifie les choses, puisque Comparator est une interface fonctionnelle.

Comparator comp = (Person p1, Person p2) -> p2.getAge() - p1.getAge();

Et le JDK 8 a introduit des méthodes qui facilite l’écriture de comparateurs.

Comparator comp = Comparator.comparing(Person::getAge).reversed();

Implémentations de List

Comparée à LinkedList, ArrayList presque toujours meilleure, quelle que soit la taille de la liste. Le seul cas où LinkedList est éventuellement meilleure, c’est quand on modifie les éléments intermédiaires de la liste : ajout ou suppression d’objets en début ou au milieu de la liste.

java.util.concurrent.CopyOnWriteArrayList est une variante thread-safe de java.util.ArrayList. Chaque méthode de modification fait une copie du tableau interne.

Implémentations de Set

L’implémentation par défaut est HashSet.

Avec LinkedHashSet, les objets sont itérés dans l’ordre de leur ajout.

TreeSet implémente SortedSet qui, comme sont nom l’indique, stocke les objets triés dans leur ordre naturel ou selon un comparateur.

EnumSet est utilisé pour les types énumérés. Il est meilleur pour ça parce qu’il ne passe pas par equals() mais ==.

Il y a deux implémentations thread-safe :

  • CopyOnWriteArraySet fait une copie de son tableau interne à chaque modification,

  • ConcurrentSkipListSet implémente SortedSet.

Il y a une troisième option, à partir d’un ConcurrentHashMap.

Set<String> set = ConcurrentHashMap.newKeySet();

Implémentations de Map

L’implémentation par défaut est HashMap.

Avec LinkedHashMap, les objets sont itérés dans l’ordre de leur ajout.

TreeMap implémente SortedMap qui, comme sont nom l’indique, stocke les clés triées dans leur ordre naturel ou selon un comparateur.

EnumMap est utilisé pour les clés énumérés. IdentityHashMap compare aussi les clés par ==, ce qui est performant mais contraire au contrat de Map.

Les implémentations thread-safe :

  • ConcurrentHashMap est l’implémentation par défaut,

  • ConcurrentSkipListMap implémente SortedMap.