Une liste de tags, d’identifiants ou de rôles ne devrait généralement pas contenir deux fois la même valeur. En Java, l’interface Set répond précisément à ce besoin, mais le choix entre HashSet, LinkedHashSet et TreeSet influence directement l’ordre, les performances et le comportement du programme. Je présente ici les différences essentielles, des exemples concrets et les erreurs à éviter.
Le bon Set dépend surtout de l’ordre et des performances attendus
- Set interdit les doublons.
- HashSet privilégie la rapidité sans garantir l’ordre.
- LinkedHashSet conserve l’ordre d’insertion.
- TreeSet trie les éléments automatiquement.
- equals() et hashCode() déterminent l’identité des objets stockés.
Comprendre l’interface Set en Java
Un Set est une collection qui ne peut contenir qu’une seule occurrence d’un même élément. Lorsque j’ajoute deux fois la chaîne "Paris", la seconde insertion ne crée pas une nouvelle valeur. La méthode add() renvoie alors false, ce qui permet de savoir que l’élément était déjà présent.
Set villes = new HashSet<>();
System.out.println(villes.add("Paris")); // true
System.out.println(villes.add("Paris")); // false
System.out.println(villes.size()); // 1
Contrairement à une List, un ensemble ne fournit pas d’accès par position. Il n’existe donc pas de méthode comme get(0). En échange, les opérations comme rechercher une valeur, l’ajouter ou la supprimer sont généralement très efficaces.
Le comportement des doublons repose sur equals() et hashCode(). Pour les types standards comme String, Integer ou UUID, le contrat est déjà correctement implémenté. Avec une classe personnelle, il faut en revanche redéfinir ces deux méthodes de manière cohérente.
Les opérations essentielles
Set langages = new HashSet<>();
langages.add("Java");
langages.add("Python");
boolean present = langages.contains("Java");
langages.remove("Python");
for (String langage : langages) {
System.out.println(langage);
}
J’utilise souvent contains() avant de modifier une structure métier, mais ce n’est pas toujours nécessaire. Dans beaucoup de cas, il est plus simple d’appeler directement add() ou remove(), puis d’examiner la valeur booléenne retournée. Cela évite une recherche supplémentaire et garde le code plus lisible.
HashSet, LinkedHashSet ou TreeSet
Ces trois implémentations couvrent la majorité des besoins courants. Leur différence la plus visible concerne l’ordre d’itération, mais le coût des opérations et les possibilités de tri comptent tout autant.
| Implémentation | Ordre des éléments | Performance habituelle | Cas d’utilisation |
|---|---|---|---|
HashSet |
Aucun ordre garanti | O(1) en moyenne | Déduplication et recherche rapide |
LinkedHashSet |
Ordre d’insertion | O(1) en moyenne | Déduplication avec affichage prévisible |
TreeSet |
Ordre naturel ou comparateur | O(log n) | Valeurs triées et recherche par intervalle |
HashSet pour la rapidité
HashSet s’appuie sur une table de hachage. C’est généralement mon choix par défaut lorsque l’application a seulement besoin d’éliminer les doublons et de tester rapidement la présence d’une valeur. Il ne faut toutefois jamais dépendre de l’ordre obtenu pendant un parcours.
Set nombres = new HashSet<>();
nombres.add(30);
nombres.add(10);
nombres.add(20);
System.out.println(nombres); // ordre non garanti
Le résultat peut sembler stable pendant les tests, puis changer après une modification du code, du nombre d’éléments ou de la version de Java. Si l’ordre compte, HashSet est le mauvais choix, même si l’affichage paraît correct sur votre machine.
LinkedHashSet pour garder l’ordre d’arrivée
LinkedHashSet conserve l’ordre dans lequel les éléments ont été ajoutés. Il convient très bien à une liste de filtres sélectionnés, à des permissions ou à des valeurs dédupliquées qui doivent rester présentées dans un ordre naturel pour l’utilisateur.
Set extensions = new LinkedHashSet<>();
extensions.add("pdf");
extensions.add("jpg");
extensions.add("pdf");
System.out.println(extensions); // [pdf, jpg]
Cette implémentation consomme un peu plus de mémoire qu’un HashSet, car elle maintient aussi une structure liée entre les entrées. Dans la plupart des applications, ce compromis est raisonnable dès qu’un résultat prévisible améliore la lisibilité ou l’expérience utilisateur.
TreeSet pour trier automatiquement
TreeSet organise les éléments selon leur ordre naturel ou selon un Comparator. Les chaînes sont triées lexicographiquement et les nombres selon leur valeur. Les opérations sont moins rapides que dans un ensemble basé sur le hachage, mais l’ordre est garanti et les recherches par intervalle deviennent possibles.
Set scores = new TreeSet<>();
scores.add(42);
scores.add(8);
scores.add(25);
System.out.println(scores); // [8, 25, 42]
J’évite d’utiliser TreeSet uniquement pour obtenir un affichage trié ponctuel. Dans ce cas, un HashSet puis un tri au moment nécessaire peut être plus adapté. En revanche, si la collection doit rester triée pendant toute sa durée de vie, TreeSet exprime mieux l’intention.
Dédupliquer une collection sans perdre les bonnes informations
L’un des usages les plus pratiques consiste à supprimer les doublons d’une liste. Il suffit de construire un ensemble à partir de la collection existante, puis de choisir l’implémentation selon l’ordre attendu.
List tags = List.of("java", "api", "java", "sécurité");
Set uniques = new LinkedHashSet<>(tags);
System.out.println(uniques); // [java, api, sécurité]
Avec HashSet, les doublons disparaissent mais l’ordre initial peut être perdu. Avec LinkedHashSet, on obtient une déduplication stable, ce qui est souvent le résultat le plus utile pour une interface ou un export. Pour un classement alphabétique, il faut plutôt utiliser TreeSet.
Lire aussi : Or en Java - Évitez les bugs et l'overflow en modélisant bien
Attention aux objets personnalisés
Deux objets ayant les mêmes données ne sont pas automatiquement considérés comme égaux si leur classe n’implémente pas correctement equals() et hashCode(). Cette erreur produit des doublons invisibles dans les ensembles et peut être difficile à repérer lorsque les objets sont affichés de la même manière.
public final class Utilisateur {
private final long id;
public Utilisateur(long id) {
this.id = id;
}
@Override
public boolean equals(Object objet) {
if (this == objet) return true;
if (!(objet instanceof Utilisateur autre)) return false;
return id == autre.id;
}
@Override
public int hashCode() {
return Long.hashCode(id);
}
}
La règle est simple, mais importante. Si deux instances sont égales selon equals(), elles doivent retourner le même hashCode. De plus, il vaut mieux éviter de modifier après insertion les champs utilisés pour calculer cette identité, car l’objet peut ensuite devenir impossible à retrouver dans le Set.
Créer des ensembles immuables et sûrs
Lorsque les valeurs ne doivent pas changer, les méthodes Set.of() et Set.copyOf() rendent l’intention très claire. Elles produisent des ensembles non modifiables et refusent les doublons dès la création.
Set rôles = Set.of("ADMIN", "LECTEUR");
Set copie = Set.copyOf(rôles);
Une tentative d’appel à add() ou remove() sur ces ensembles déclenche une UnsupportedOperationException. C’est une protection utile pour les constantes et les paramètres de configuration, mais il ne faut pas les confondre avec une copie défensive destinée à être modifiée plus tard.
Les méthodes Set.of() refusent également les valeurs null. HashSet et LinkedHashSet acceptent généralement un seul élément nul, tandis que TreeSet dépend de son comparateur et de son ordre de comparaison. Dans du code applicatif, je préfère souvent interdire null explicitement afin d’éviter des comportements différents selon l’implémentation choisie.
Pour un accès concurrent, un simple HashSet n’est pas adapté. Selon le scénario, on peut utiliser ConcurrentHashMap.newKeySet() pour un ensemble partagé entre plusieurs threads, ou CopyOnWriteArraySet lorsque les lectures sont très fréquentes et les modifications rares. Ces solutions répondent à des contraintes précises et ne doivent pas être choisies uniquement parce que le mot « concurrent » semble plus sûr.
Les erreurs qui rendent un Set difficile à maintenir
La première erreur consiste à attendre un ordre déterminé d’un HashSet. La deuxième est de modifier la collection pendant un parcours classique, ce qui peut provoquer une ConcurrentModificationException.
Set valeurs = new HashSet<>();
valeurs.add("A");
valeurs.add("B");
Iterator itérateur = valeurs.iterator();
while (itérateur.hasNext()) {
String valeur = itérateur.next();
if (valeur.equals("A")) {
itérateur.remove();
}
}
Pour filtrer une collection, la méthode removeIf() est souvent plus simple et plus lisible. Avec les flux, on peut aussi créer un nouvel ensemble, mais il faut choisir explicitement l’implémentation si l’ordre compte.
Set filtrées = valeurs.stream()
.filter(valeur -> valeur.length() > 1)
.collect(Collectors.toCollection(LinkedHashSet::new));
Enfin, il ne faut pas utiliser un Set quand les doublons sont significatifs ou quand l’accès par position est central. Une playlist, une file d’événements ou une suite de résultats peut nécessiter une List. Le bon critère n’est pas la popularité de la collection, mais la règle métier que le code doit représenter.
Choisir rapidement la bonne implémentation
Dans un nouveau projet, je pars généralement de trois questions simples. Ai-je besoin d’un ordre, les éléments doivent-ils être triés, et la collection sera-t-elle partagée entre plusieurs threads ? Les réponses orientent presque immédiatement le choix.
- Choisissez HashSet pour une recherche rapide sans ordre requis.
- Choisissez LinkedHashSet pour conserver l’ordre d’insertion.
- Choisissez TreeSet pour un ensemble toujours trié.
- Choisissez Set.of() pour des valeurs fixes et immuables.
- Choisissez une collection concurrente uniquement si plusieurs threads accèdent réellement aux données.
Ce choix paraît modeste, mais il évite des corrections coûteuses plus tard. Une collection bien choisie rend le contrat du code visible immédiatement, améliore la fiabilité des tests et limite les surprises lors de l’affichage ou de la sérialisation.
Le réflexe qui évite la plupart des mauvais choix
Un Set est avant tout une déclaration d’intention. Il signifie que chaque élément doit être unique, tandis que l’implémentation précise si l’ordre est libre, conservé ou trié.
Pour commencer, utilisez HashSet lorsque seul le caractère unique compte. Passez à LinkedHashSet dès que l’ordre d’arrivée a une valeur fonctionnelle, et à TreeSet lorsque le tri permanent ou les recherches par intervalle justifient leur coût supplémentaire.
Le point le plus souvent oublié reste la cohérence de equals() et hashCode(). Si cette règle est respectée et que l’implémentation correspond au besoin réel, les ensembles Java deviennent une solution simple, rapide et très fiable.