Comparació d’objectes
- Comparable: l’ordre natural
- Comparator: ordres alternatius
- Quin dels dos
- On es fa servir l’ordre
- Referències
Tenim una llista de persones i la volem ordenada:
List<Persona> persones = new ArrayList<>();
Collections.sort(persones); // error de compilació
Java no pot endevinar què vol dir que una persona sigui “menor” que una altra. Li ho hem de dir, i hi ha dues maneres de fer-ho:
- Fer la classe comparable, quan té un ordre propi i evident.
- Escriure un comparador a part, quan l’ordre és un entre molts de possibles o quan no podem tocar la classe.
Comparable: l’ordre natural
Una classe és comparable si implementa la interfície Comparable<T>, que té un únic mètode:
int compareTo(T o)
No retorna un booleà, sinó un enter del qual només importa el signe:
| Retorn | Significat |
|---|---|
| Negatiu | Aquest objecte va abans que o |
| Zero | Els dos van a la mateixa posició |
| Positiu | Aquest objecte va després que o |
Amb això, la classe ja té un ordre natural:
public class Persona implements Comparable<Persona> {
private final String cognom;
private final int edat;
@Override
public int compareTo(Persona altra) {
return this.cognom.compareTo(altra.cognom);
}
}
Ara Collections.sort(persones) compila i ordena per cognom.
Moltes classes de la biblioteca estàndard ja són comparables: String per ordre alfabètic, les classes embolcall dels tipus primitius (Integer, Double, Character) per ordre numèric, i LocalDate o LocalDateTime per ordre cronològic. Per això una List<String> es pot ordenar sense fer res més.
Dos detalls del contracte
No implementis compareTo() amb una resta. És el costum heretat de C i pot desbordar amb valors extrems, retornant el signe equivocat:
return this.edat - altra.edat; // pot desbordar
return Integer.compare(this.edat, altra.edat); // correcte
L’ordre natural hauria de ser coherent amb equals(). No és obligatori, però si no ho és les col·leccions ordenades es comporten de manera sorprenent: un TreeSet i un TreeMap decideixen si dos elements són el mateix amb compareTo(), no amb equals().
Set<Persona> conjunt = new TreeSet<>();
conjunt.add(new Persona("Puig", 30));
conjunt.add(new Persona("Puig", 45)); // no s'afegeix: compareTo() retorna 0
conjunt.size(); // 1
Amb un HashSet hi hauria les dues persones, perquè allà mana equals(). Si l’ordre natural només mira el cognom, val més que equals() també, o que la comparació desempati amb algun camp més.
Comparator: ordres alternatius
Un Comparator<T> és un objecte a part que sap comparar dos elements. També té un únic mètode, amb el mateix conveni de signes:
int compare(T o1, T o2)
La manera pràctica d’escriure’n un no és implementar la interfície, sinó fer servir els constructors de Comparator, que reben el camp pel qual vols ordenar:
persones.sort(Comparator.comparing(Persona::getCognom));
persones.sort(Comparator.comparingInt(Persona::getEdat));
Es poden encadenar per desempatar i invertir:
persones.sort(Comparator.comparing(Persona::getCognom)
.thenComparing(Persona::getNom)
.reversed());
Fes servir comparingInt(), comparingLong() i comparingDouble() quan el camp és primitiu: eviten l’autoboxing de cada comparació.
Si algun camp pot ser null, embolcalla el comparador amb Comparator.nullsFirst() o nullsLast(), o rebràs un NullPointerException enmig de l’ordenació.
Quin dels dos
| Situació | Tria |
|---|---|
| La classe té un ordre evident i únic (un import, una data) | Comparable |
| Hi ha diversos criteris raonables (per cognom, per edat, per data d’alta) | Comparator |
| La classe no és teva i no la pots modificar | Comparator |
| Vols l’ordre per defecte, però en un cas concret te’n cal un altre | Tots dos: ordre natural i un comparador per a l’excepció |
En general val més fer la classe comparable si l’ordre forma part del que significa la classe. Un comparador és millor quan l’ordre depèn de qui la fa servir i no de la classe mateixa.
On es fa servir l’ordre
Un cop tens l’ordre definit, el fan servir moltes parts de la biblioteca:
list.sort(cmp)iCollections.sort(list), entre altres algorismes.- Les col·leccions ordenades
TreeSetiTreeMap, i tambéPriorityQueue. Totes tenen un constructor que accepta unComparator, i sense ell fan servir l’ordre natural. - Les operacions
sorted(),max()imin()de l’Stream API.