Dans cet article (partie 3 de la série sur les collections), nous allons nous concentrer sur la famille Set du Framework Collections : ses caractéristiques, ses principales implémentations (HashSet, LinkedHashSet, TreeSet, EnumSet…), leurs différences, pièges courants et bonnes pratiques d’utilisation.
- Introduction aux collections Java
- Les listes (List) en Java
- Les ensembles (Set) en Java (vous êtes ici)
- Les files (Queue) et Deques en Java
- Les maps (Map) en Java
- Utilisations avancées des collections (article à venir)
Qu’est‑ce qu’un Set ?
Set est une sous‑interface de Collection qui représente un ensemble d’éléments sans doublon.
- Unicité des éléments (pas de doublons).
- Pas d’accès par index (contrairement à
List). - Notion d’ordre dépend de l’implémentation (pas ordre, selon l’ordre d’insertion, ou trié).
Signature :
public interface Set<E> extends Collection<E> { /* … */ }
L’unicité est définie par :
- Pour les ensembles à base de hachage (
HashSet,LinkedHashSet) : la combinaison dehashCode()etequals()des éléments. - Pour les ensembles triés (
TreeSet) : l’ordre imposé parComparator/Comparable(deux éléments considérés « égaux » sicompare(a, b) == 0).
Méthodes de l’interface Set
En plus des méthodes héritées de Collection, voici les méthodes les plus utilisées avec leurs particularités pour Set :
| Méthode | Description |
|---|---|
| boolean add(E e) | Ajoute l’élément s’il n’est pas déjà présent. Retourne true si le set a changé (doublons ignorés). |
| boolean addAll(Collection<? extends E> c) | Ajoute les éléments non présents de c. Les doublons sont ignorés. Retourne true si le set a changé. |
| boolean contains(Object o) | true si l’élément est présent (défini par equals/hashCode, ou par l’ordre du comparateur pour TreeSet). |
| boolean containsAll(Collection<?> c) | true si tous les éléments de c sont présents. |
| boolean remove(Object o) | Supprime l’élément s’il est présent. Retourne true si un élément a été retiré. |
| boolean removeAll(Collection<?> c) | Supprime tous les éléments présents dans c. |
| boolean retainAll(Collection<?> c) | Ne garde que les éléments aussi présents dans c (intersection). |
| void clear() | Vide le set. |
| int size() | Nombre d’éléments. |
| boolean isEmpty() | true si vide. |
| Iterator |
Itérateur sur le set (ordre selon l’implémentation : insertion, aucun, trié). |
| Object[] toArray() | Copie les éléments dans un tableau Object[]. |
| Copie les éléments dans un tableau typé fourni. | |
| boolean equals(Object o) | Égalité de contenu (indépendante de l’ordre pour les ensembles). |
| int hashCode() | Hash basé sur les éléments (somme des hashCodes des éléments). |
| static |
Crée un set immuable (Java 9+). Rejette les doublons (IllegalArgumentException). |
| static |
Crée une copie immuable d’une collection (Java 10+). Rejette les doublons. |
Remarques :
add/addAlln’ajoutent jamais de doublons ; le booléen indique si le set a effectivement changé.- Le contrat de
Set.equalsne dépend pas de l’ordre d’itération : deux sets égaux contiennent les mêmes éléments. - Les usines immuables (
Set.of,Set.copyOf) rejettent toute tentative de doublon à la création.
Principales implémentations
HashSet
- Structure : table de hachage.
- Pas d’ordre garanti d’itération.
- Opérations de base (
add,contains,remove) enO(1). - Autorise un seul
null.
Cas d’usage : ensemble générique sans contrainte d’ordre, rapide par défaut.
LinkedHashSet
- Même base que
HashSet+ chaîne de maillons pour l’ordre d’insertion (ou ordre d’accès si activé côtéLinkedHashMap). - Itération prévisible (ordre d’insertion).
- Surcoût mémoire minime par rapport à
HashSet.
Cas d’usage : vous voulez la rapidité d’un HashSet avec un ordre d’itération stable (journalisation, export, UI).
TreeSet (NavigableSet)
- Structure : arbre rouge‑noir (trié).
- Itération triée selon l’ordre naturel ou un
Comparatorfourni au constructeur. - Opérations en
O(log n). - Ne supporte pas
nullavec l’ordre naturel. - Fournit l’API
NavigableSet:first,last,lower,floor,ceiling,higher,subSet,headSet,tailSet…
Cas d’usage : vous avez besoin d’un ensemble trié ou d’opérations par plage.
EnumSet
- Ensemble ultra‑optimisé pour des constantes d’une même
enum(Utilise un bitset en interne). - Très compact et rapide, itération dans l’ordre de déclaration des constantes.
- Ne supporte que des éléments d’une seule énumération.
Cas d’usage : flags/options, états, rôles… Exemple : EnumSet.of(READ, WRITE).
CopyOnWriteArraySet
- Thread‑safe, basé sur copie lors d’écriture (comme
CopyOnWriteArrayList). - Lectures très rapides et sans verrou, chaque mutation crée une nouvelle copie interne.
- À éviter si beaucoup d’écritures.
Cas d’usage : beaucoup de lectures, peu d’écritures, besoins de sécurité de thread simple.
ConcurrentSkipListSet
- Version concurrente et triée (structure de skip‑list).
- Semantique proche d’un
TreeSetthread‑safe, avec coûts légèrement supérieurs.
Cas d’usage : ensemble trié partagé entre threads sans verrou global.
Complexités
HashSet/LinkedHashSet:add/contains/remove≈O(1).TreeSet/ConcurrentSkipListSet:O(log n).EnumSet: très proche deO(1)pour la majorité des opérations.
Opérations de base
Set<String> tags = new HashSet<>();
boolean added = tags.add("java"); // true si l’élément n’était pas présent
added = tags.add("java"); // false (doublon ignoré)
boolean present = tags.contains("java"); // true
boolean removed = tags.remove("java"); // true
Opérations ensemblistes (union, intersection, différence)
Set<Integer> a = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> b = new HashSet<>(Set.of(3, 4));
// union: a ∪ b => {1,2,3,4}
Set<Integer> union = new HashSet<>(a);
union.addAll(b);
// intersection: a ∩ b => {3}
Set<Integer> inter = new HashSet<>(a);
inter.retainAll(b);
// différence: a − b => {1,2}
Set<Integer> diff = new HashSet<>(a);
diff.removeAll(b);
Ordre d’itération et tri
HashSet: aucun ordre garanti.LinkedHashSet: ordre d’insertion (stable).TreeSet: ordre trié (naturel ou comparateur fourni).
Exemple TreeSet :
record User(String username, int score) {}
// Tri par score décroissant, puis username
Comparator<User> byScoreDescThenName =
Comparator.comparingInt(User::score).reversed()
.thenComparing(User::username);
Set<User> leaderboard = new TreeSet<>(byScoreDescThenName);
leaderboard.add(new User("alice", 42));
leaderboard.add(new User("bob", 42));
leaderboard.add(new User("carl", 10));
// itération triée selon le comparateur
leaderboard.forEach(System.out::println);
Attention : dans un TreeSet, deux éléments a et b sont considérés comme doublons si compare(a, b) == 0, même si a.equals(b) vaut false. Assurez‑vous que le comparateur est cohérent avec equals quand c’est important.
Unicité : equals et hashCode
Pour HashSet/LinkedHashSet, l’unicité repose sur hashCode et equals. Quelques règles :
- Si vous redéfinissez
equals, redéfinissez aussihashCodeavec une logique compatible. - N’utilisez pas des champs mutables participant au
hashCodesi ces champs peuvent changer après insertion dans le set.
Exemple de piège :
class Person {
String ssn; // utilisé dans equals/hashCode
String name;
// equals/hashCode basés sur ssn
}
Set<Person> s = new HashSet<>();
Person p = new Person();
p.ssn = "123";
s.add(p);
p.ssn = "999"; // le hashCode change
boolean contains = s.contains(p); // peut retourner false !
Solution : rendez immuables les champs identifiants (ou n’incluez pas de champs mutables dans equals/hashCode).
Immutabilité
- Construire un set immuable :
Set<String> roles = Set.of("ADMIN", "USER"); // Java 9+
- Exposer une vue non modifiable d’un set interne :
class Service {
private final Set<String> scopes = new HashSet<>();
public Set<String> getScopes() {
return Collections.unmodifiableSet(scopes);
}
}
Déduplication et conversion
- Dédupliquer une liste :
List<String> emails = List.of("a@x", "b@x", "a@x");
Set<String> unique = new LinkedHashSet<>(emails); // conserve l’ordre d’insertion
- Recréer une liste sans doublons (même ordre) :
List<String> dedup = new ArrayList<>(new LinkedHashSet<>(emails));
Concurrence
- Besoin simple de synchronisation :
Collections.synchronizedSet(new HashSet<>()). - Beaucoup de lectures, peu d’écritures :
CopyOnWriteArraySet. - Ensemble trié concurrent :
ConcurrentSkipListSet. - Évitez d’exposer des sets mutables partagés ; privilégiez l’immuabilité ou le confinement par thread.
Bonnes pratiques
- Déclarez par l’interface :
Set<Foo> s = new HashSet<>(); - Choisissez l’implémentation selon vos besoins :
- Pas d’ordre, perfs par défaut :
HashSet. - Ordre d’insertion stable :
LinkedHashSet. - Ordre trié ou requêtes par plage :
TreeSet. - Enumérations :
EnumSet.
- Pas d’ordre, perfs par défaut :
- Attention aux éléments mutables impliqués dans
equals/hashCode. - Pour un ordre d’itération stable sans coût du tri :
LinkedHashSetplutôt queTreeSet. - Utilisez
removeIf,addAll,retainAll,removeAllpour exprimer des opérations ensemblistes clairement.
Quand préférer List, Set ou Map ?
Set: vous devez garantir l’unicité des éléments, l’ordre n’est pas la priorité (ou dépend de l’implémentation choisie).List: l’ordre et les doublons comptent.Map: vous avez besoin d’associer chaque clé à une valeur.
Conclusion
Pour conclure, il faut retenir que les ensembles (Set) garantissent l’unicité des éléments, il faut cependant ne pas oublier d’implémenter equals/hashCode ou compareTo.
Utiliser :
HashSetpar défautLinkedHashSetsi l’ordre d’insertion compteTreeSetsi un tri ou des bornes sont nécessairesEnumSetpour les énumérations- Les variantes concurrentes si plusieurs threads partagent la structure.
Pour aller plus loin dans la série :
- Introduction aux collections Java
- Les listes (List) en Java
- Les ensembles (Set) en Java (vous êtes ici)
- Les files (Queue) et Deques en Java
- Les maps (Map) en Java
- Utilisations avancées des collections (article à venir)