Java Comparable et Comparator
Définissez l'ordre naturel avec Comparable et l'ordre externe avec Comparator en Java, et composez des comparateurs.
Deux interfaces, un seul objectif : indiquer à Java quand un objet est « inférieur » à un autre. Elles se ressemblent presque à l'utilisation, et leurs méthodes retournent même la même forme de valeur — un int négatif, zéro, ou un int positif. La différence tient à l'endroit où réside l'ordre :
Comparable<T>— le type lui-même sait comment ordonner ses instances. Sa méthodeint compareTo(T other)définit l'ordre naturel du type.Comparator<T>— un objet externe qui ordonne des instances. Sa méthodeint compare(T a, T b)décrit un parmi de nombreux ordres possibles.
Vous implémentez Comparable quand il existe un « inférieur » évident pour un type — Integer, String, LocalDate. Vous écrivez un Comparator pour tout autre ordre — par longueur, par nom insensible à la casse, par prix décroissant, par n'importe quoi que vous pouvez exprimer en code. La plupart des types ont un seul Comparable (ou aucun) et des dizaines de Comparators utiles.
Le contrat : −/0/+
Les deux méthodes retournent un int dont le signe est la réponse :
- négatif —
avient avantb - zéro — égaux pour les besoins du tri
- positif —
avient aprèsb
La magnitude exacte n'a pas d'importance. -1 et -1_000_000 signifient la même chose. N'utilisez jamais return a.size - b.size quand un débordement est possible : soustraire Integer.MIN_VALUE d'un nombre positif provoque un dépassement. Utilisez plutôt Integer.compare(a.size(), b.size()) — il est sûr vis-à-vis des débordements et comporte le même nombre de caractères à taper.
Comparable<T> — ordre naturel
Un type implémente Comparable<Self> et fournit compareTo :
public record Version(int major, int minor, int patch) implements Comparable<Version> {
@Override public int compareTo(Version other) {
int m = Integer.compare(this.major, other.major);
if (m != 0) return m;
int n = Integer.compare(this.minor, other.minor);
if (n != 0) return n;
return Integer.compare(this.patch, other.patch);
}
}Maintenant Collections.sort(versions), versions.stream().sorted(), new TreeSet<Version>() et new TreeMap<Version, X>() fonctionnent tous sans avoir à passer d'argument supplémentaire.
Le contrat comporte trois règles que tout compareTo doit respecter :
- Anti-symétrique —
a.compareTo(b)etb.compareTo(a)ont des signes opposés. - Transitif — si
a < betb < c, alorsa < c. - Cohérent avec
equals(fortement recommandé) —a.compareTo(b) == 0si et seulement sia.equals(b).
La troisième règle est celle que les gens violent par inadvertance. BigDecimal en est l'exemple célèbre : new BigDecimal("1.0").compareTo(new BigDecimal("1.00")) vaut 0, mais .equals retourne false. En conséquence, un TreeSet<BigDecimal> et un HashSet<BigDecimal> seront en désaccord sur le fait que "1.0" et "1.00" sont des doublons. Si possible, gardez-les cohérents.
Comparator<T> — ordre externe
Un Comparator est un objet séparé. Il peut comparer deux T quelconques, y compris des types que vous n'avez pas écrits :
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
list.sort(byLength);Puisque Comparator<T> est une interface fonctionnelle (une seule méthode abstraite, compare), tout Comparator n'est qu'un lambda ou une référence de méthode. C'est la forme moderne du code de comparateur — vous n'écrivez presque plus jamais de classe anonyme complète.
Les constructeurs de Comparator
La classe dispose de méthodes factory statiques qui rendent la construction de comparateurs courte et lisible :
Comparator<Person> byAge = Comparator.comparingInt(Person::age);
Comparator<Person> byName = Comparator.comparing(Person::name);
Comparator<Person> byNameCi = Comparator.comparing(Person::name, String.CASE_INSENSITIVE_ORDER);
Comparator<Person> oldestFirst = byAge.reversed();
Comparator<String> nullsFirst = Comparator.nullsFirst(Comparator.naturalOrder());Utilisez les constructeurs spécialisés pour les primitives — comparingInt, comparingLong, comparingDouble — quand la clé est une primitive. Ils évitent le boxing à chaque comparaison, ce qui s'accumule sur un tri long.
Comparateurs chaînés avec thenComparing
L'autre raison de préférer les constructeurs : vous pouvez chaîner plusieurs clés.
Comparator<Person> ordering =
Comparator.comparing(Person::lastName)
.thenComparing(Person::firstName)
.thenComparingInt(Person::age);Cela se lit de haut en bas : « clé primaire : nom de famille ; départage par prénom ; puis par âge. » thenComparing est invoqué sur le comparateur précédent et retourne un nouveau comparateur qui ne consulte la seconde clé que si la première a signalé une égalité. La chaîne n'a pas de limite.
reversed(), nullsFirst, nullsLast
Trois modificateurs reviennent constamment :
reversed()inverse l'ordre de tout comparateur.byAge.reversed()signifie « les plus âgés d'abord. »nullsFirst(cmp)enveloppe un comparateur de sorte que les valeursnullsoient traitées comme inférieures à toute valeur non nulle. Utile pour trier des collections pouvant contenir desnull.nullsLast(cmp)est le pendant symétrique.
N'utilisez pas reversed() sur un comparateur chaîné en espérant que seule la dernière clé soit inversée — reversed() inverse l'intégralité de l'ordre, chaque clé dans la chaîne.
Comparable vs Comparator dans les API du JDK
De nombreuses méthodes existent en deux versions — l'une utilisant l'ordre naturel, l'autre prenant un Comparator :
| Opération | Surcharge ordre naturel | Surcharge Comparator |
|---|---|---|
| Trier une liste | Collections.sort(list) | Collections.sort(list, cmp) |
| Trier une liste (moderne) | list.sort(null) | list.sort(cmp) |
| Trier un stream | stream.sorted() | stream.sorted(cmp) |
| Ensemble basé sur un arbre | new TreeSet<>() | new TreeSet<>(cmp) |
| Map basée sur un arbre | new TreeMap<>() | new TreeMap<>(cmp) |
| Min/max | Collections.min(list) | Collections.min(list, cmp) |
| Recherche binaire | Collections.binarySearch(list, key) | Collections.binarySearch(list, key, cmp) |
PriorityQueue | ordre naturel du type d'élément | le constructeur prend un Comparator |
Les formes à ordre naturel exigent que le type d'élément implémente Comparable. Si ce n'est pas le cas et que vous les appelez quand même, vous obtiendrez une ClassCastException à l'exécution — et non une erreur de compilation — car le cast se produit à l'intérieur de l'implémentation du tri.
Un exemple concret : ordre naturel, comparateurs personnalisés, clés chaînées, nulls
Le programme ci-dessous définit un record avec un ordre naturel (Comparable) et trois ordres externes : par une clé unique, par des clés chaînées avec un secondaire inversé, et un ordre qui tolère les entrées null.
Ce qu'il faut retenir de l'exécution :
- L'implémentation
Comparablea trié par nom et départagé les égalités de nom par âge. Aucun comparateur explicite n'était nécessaire — l'ordre naturel est le comportement par défaut pourCollections.sortet ses variantes. Comparator.comparingDouble(Person::salary)est plus court et plus rapide qu'écrire(a, b) -> Double.compare(a.salary(), b.salary())car il évite le boxing.- Le comparateur chaîné a trié principalement par âge et utilisé
reversed()uniquement sur la partie salaire — c'est le bon schéma quand vous souhaitez des directions différentes sur des clés différentes. Comparez avec l'appel de.reversed()sur toute la chaîne, qui inverserait les deux clés. nullsFirsta permis au comparateur de traiter une liste contenant des entréesnullsansNullPointerException. Sans cet enveloppeur, la première comparaison impliquant unnullaurait planté.- L'« astuce de la soustraction » a produit la mauvaise réponse pour
Integer.MAX_VALUE - (-1): ce calcul déborde vers un nombre négatif, doncbadsignale queMAX_VALUEest inférieur à-1.Integer.compareproduit le bon signe à chaque fois. Préférez-le toujours.
La suite
Vous maîtrisez maintenant l'itération (Iterator / ListIterator) et l'ordre (Comparable / Comparator). Le prochain chapitre les réunit dans la classe utilitaire java.util.Collections — la boîte à outils statique de tri, recherche, inversion, mélange, min, max et « envelopper cette collection en immuable » qui opère sur tout List, Set ou Map. Ensuite, deux courts chapitres abordent spécifiquement le tri et la recherche.