Aller au contenu principal

Comparable et Comparator

On a des ArrayList<Student>, des listes de produits, des collections d'objets — mais comment les trier par moyenne, par prix, par nom ? Java fournit deux interfaces génériques pour ça : Comparable<T> et Comparator<T>.

Comparable<T> — ordre naturel

L'interface Comparable<T> permet à une classe de définir son ordre naturel — la façon dont ses objets se comparent entre eux par défaut.

public interface Comparable<T> {
int compareTo(T autre);
}

La méthode compareTo() retourne :

  • un nombre négatif si this est inférieur à autre
  • zéro s'ils sont égaux
  • un nombre positif si this est supérieur à autre

Exemple : Student trié par moyenne

public class Student implements Comparable<Student> {
private String name;
private int age;
private double average;

public Student(String name, int age, double average) {
this.name = name;
this.age = age;
this.average = average;
}

public String getName() { return name; }
public int getAge() { return age; }
public double getAverage() { return average; }

@Override
public int compareTo(Student autre) {
return Double.compare(this.average, autre.average);
}

@Override
public String toString() {
return name + " (" + average + ")";
}
}

Une fois Comparable implémenté, Collections.sort() sait quoi faire :

List<Student> students = new ArrayList<>(List.of(
new Student("Alice", 20, 85.5),
new Student("Bob", 22, 76.3),
new Student("Charlie", 19, 91.0)
));

Collections.sort(students);

students.forEach(System.out::println);
// Bob (76.3)
// Alice (85.5)
// Charlie (91.0)

Double.compare() plutôt que la soustraction

// MAUVAIS — imprécis avec des double, risque de débordement avec des int
return (int)(this.average - autre.average);

// BON
return Double.compare(this.average, autre.average);

Pour inverser l'ordre (décroissant), il suffit d'inverser les arguments :

return Double.compare(autre.average, this.average); // tri décroissant

Comparator<T> — ordre externe

Comparable définit un seul ordre naturel dans la classe. Mais si on veut trier les mêmes objets de plusieurs façons (par moyenne, par nom, par âge…), on utilise Comparator<T>.

Puisque Comparator<T> est une interface fonctionnelle, on peut l'écrire avec une lambda :

// Trier par nom alphabétique
students.sort((s1, s2) -> s1.getName().compareTo(s2.getName()));

// Trier par moyenne décroissante
students.sort((s1, s2) -> Double.compare(s2.getAverage(), s1.getAverage()));

Définir les comparateurs dans la classe

Comme vu en 4.2, on peut définir les Comparator comme constantes statiques directement dans Student :

public class Student implements Comparable<Student> {
// ...

public static final Comparator<Student> PAR_NOM =
(s1, s2) -> s1.getName().compareTo(s2.getName());

public static final Comparator<Student> PAR_MOYENNE_DESC =
(s1, s2) -> Double.compare(s2.getAverage(), s1.getAverage());
}
students.sort(Student.PAR_NOM);
students.sort(Student.PAR_MOYENNE_DESC);

Tri sur plusieurs critères

Une lambda peut contenir plusieurs instructions pour définir un critère de départage :

// Moyenne décroissante, et en cas d'égalité, par nom alphabétique
students.sort((s1, s2) -> {
int compareMoyenne = Double.compare(s2.getAverage(), s1.getAverage());
if (compareMoyenne != 0) return compareMoyenne;
return s1.getName().compareTo(s2.getName());
});

Résumé : quand utiliser quoi

Comparable<T>Comparator<T>
Dans la classe elle-mêmeLambda ou constante statique
OrdreUn seul ordre naturelAutant d'ordres qu'on veut
MéthodecompareTo(T autre)compare(T o1, T o2)
UtilisationCollections.sort(liste)liste.sort(comparateur)
Idéal pourOrdre évident (ex: moyenne, date)Tris multiples

Lien avec les génériques

Comparable<T> et Comparator<T> sont des interfaces génériques — exactement le même mécanisme vu au Cours 1. Le T garantit qu'on compare des objets du même type.

On peut écrire une méthode générique qui fonctionne avec n'importe quel type qui implémente Comparable :

// <T extends Comparable<T>> signifie : T doit savoir se comparer à lui-même
public static <T extends Comparable<T>> T trouverMax(ArrayList<T> liste) {
T max = liste.get(0);
for (T element : liste) {
if (element.compareTo(max) > 0) {
max = element;
}
}
return max;
}

Cette méthode fonctionne avec Integer, String, Student — n'importe quel type qui implémente Comparable :

ArrayList<Student> students = new ArrayList<>(List.of(
new Student("Alice", 20, 85.5),
new Student("Bob", 22, 76.3),
new Student("Charlie", 19, 91.0)
));

System.out.println(trouverMax(students)); // Charlie (91.0)