W3docs

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éthode int compareTo(T other) définit l'ordre naturel du type.
  • Comparator<T> — un objet externe qui ordonne des instances. Sa méthode int 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égatifa vient avant b
  • zéro — égaux pour les besoins du tri
  • positifa vient après b

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 :

  1. Anti-symétriquea.compareTo(b) et b.compareTo(a) ont des signes opposés.
  2. Transitif — si a < b et b < c, alors a < c.
  3. Cohérent avec equals (fortement recommandé)a.compareTo(b) == 0 si et seulement si a.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 valeurs null soient traitées comme inférieures à toute valeur non nulle. Utile pour trier des collections pouvant contenir des null.
  • 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érationSurcharge ordre naturelSurcharge Comparator
Trier une listeCollections.sort(list)Collections.sort(list, cmp)
Trier une liste (moderne)list.sort(null)list.sort(cmp)
Trier un streamstream.sorted()stream.sorted(cmp)
Ensemble basé sur un arbrenew TreeSet<>()new TreeSet<>(cmp)
Map basée sur un arbrenew TreeMap<>()new TreeMap<>(cmp)
Min/maxCollections.min(list)Collections.min(list, cmp)
Recherche binaireCollections.binarySearch(list, key)Collections.binarySearch(list, key, cmp)
PriorityQueueordre naturel du type d'élémentle 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.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • L'implémentation Comparable a 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 pour Collections.sort et 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.
  • nullsFirst a permis au comparateur de traiter une liste contenant des entrées null sans NullPointerException. Sans cet enveloppeur, la première comparaison impliquant un null aurait 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, donc bad signale que MAX_VALUE est inférieur à -1. Integer.compare produit 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.

Exercices

Pratique
Vous écrivez `list.sort((a, b) -> a.scoreDifference(b))` où `scoreDifference` retourne `a.score - b.score` sous forme d'`int`. La liste contient des scores incluant `Integer.MAX_VALUE` et `Integer.MIN_VALUE`, et le résultat est clairement erroné. Quel est le correctif ?
Vous écrivez `list.sort((a, b) -> a.scoreDifference(b))` où `scoreDifference` retourne `a.score - b.score` sous forme d'`int`. La liste contient des scores incluant `Integer.MAX_VALUE` et `Integer.MIN_VALUE`, et le résultat est clairement erroné. Quel est le correctif ?
Was this page helpful?