W3docs

Java Comparable e Comparator

Definisci l'ordinamento naturale con Comparable e quello esterno con Comparator in Java, e componi i comparatori.

Due interfacce, un solo compito: dire a Java quando un oggetto è "minore di" un altro. Sembrano quasi identiche al punto di utilizzo, e i loro metodi restituiscono persino lo stesso tipo di valore — un int negativo, zero o un int positivo. La differenza è dove risiede l'ordinamento:

  • Comparable<T> — il tipo stesso sa come ordinare le proprie istanze. Il suo metodo int compareTo(T other) rappresenta l'ordinamento naturale del tipo.
  • Comparator<T> — un oggetto esterno che ordina le istanze. Il suo metodo int compare(T a, T b) descrive uno dei molti ordinamenti possibili.

Si implementa Comparable quando esiste un ovvio "minore di" per un tipo — Integer, String, LocalDate. Si scrive un Comparator per ogni altro ordinamento — per lunghezza, per nome senza distinzione maiuscole/minuscole, per prezzo decrescente, per qualsiasi cosa esprimibile in codice. La maggior parte dei tipi ha un solo Comparable (o nessuno) e decine di Comparator utili.

Il contratto: −/0/+

Entrambi i metodi restituiscono un int il cui segno è la risposta:

  • negativoa viene prima di b
  • zero — uguali ai fini dell'ordinamento
  • positivoa viene dopo b

L'entità esatta non ha importanza. -1 e -1_000_000 significano la stessa cosa. Non usare mai return a.size - b.size quando è possibile un overflow: sottrarre Integer.MIN_VALUE da un numero positivo provoca un avvolgimento. Usare invece Integer.compare(a.size(), b.size()) — è sicuro rispetto all'overflow e richiede lo stesso numero di caratteri da digitare.

Comparable<T> — ordinamento naturale

Un tipo implementa Comparable<Self> e fornisce 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);
  }
}

Ora Collections.sort(versions), versions.stream().sorted(), new TreeSet<Version>() e new TreeMap<Version, X>() funzionano tutti correttamente senza passare argomenti aggiuntivi.

Il contratto ha tre regole che ogni compareTo deve rispettare:

  1. Anti-simmetricoa.compareTo(b) e b.compareTo(a) hanno segni opposti.
  2. Transitivo — se a < b e b < c, allora a < c.
  3. Coerente con equals (fortemente consigliato)a.compareTo(b) == 0 se e solo se a.equals(b).

La terza regola è quella che le persone violano per errore. BigDecimal è l'esempio famoso: new BigDecimal("1.0").compareTo(new BigDecimal("1.00")) restituisce 0, ma .equals restituisce false. Di conseguenza, un TreeSet<BigDecimal> e un HashSet<BigDecimal> non saranno d'accordo sul fatto che "1.0" e "1.00" siano duplicati. Se possibile, mantenerli coerenti.

Comparator<T> — ordinamento esterno

Un Comparator è un oggetto separato. Può confrontare qualsiasi coppia di T, inclusi i tipi che non hai scritto tu:

Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
list.sort(byLength);

Poiché Comparator<T> è un'interfaccia funzionale (un solo metodo astratto, compare), ogni Comparator è semplicemente una lambda o un riferimento a metodo. Questa è la forma moderna del codice con comparatori — ormai quasi nessuno scrive più una classe anonima completa.

I builder su Comparator

La classe dispone di metodi factory statici che rendono la costruzione dei comparatori breve e leggibile:

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());

Usare i builder specializzati per i primitivi — comparingInt, comparingLong, comparingDouble — quando la chiave è un primitivo. Evitano il boxing a ogni confronto, il che fa la differenza su ordinamenti lunghi.

Comparatori concatenati con thenComparing

L'altro motivo per preferire i builder: è possibile concatenare più chiavi.

Comparator<Person> ordering =
    Comparator.comparing(Person::lastName)
              .thenComparing(Person::firstName)
              .thenComparingInt(Person::age);

Questo si legge dall'alto verso il basso come "chiave primaria cognome; in caso di parità per nome; poi per età." thenComparing viene invocato sul comparatore precedente e restituisce un nuovo comparatore che consulta la seconda chiave solo quando la prima ha riportato un pareggio. Non esiste un limite alla catena.

reversed(), nullsFirst, nullsLast

Tre modificatori ricorrono di frequente:

  • reversed() inverte l'ordine di qualsiasi comparatore. byAge.reversed() significa "i più vecchi prima."
  • nullsFirst(cmp) avvolge un comparatore in modo che i valori null siano trattati come minori di qualsiasi non-null. Utile quando si ordinano collezioni che possono contenere null.
  • nullsLast(cmp) è il complemento simmetrico.

Non usare reversed() su un comparatore concatenato aspettandosi che venga invertita solo l'ultima chiave — reversed() inverte l'intero ordinamento, ogni chiave della catena.

Comparable vs Comparator nelle API JDK

Molti metodi esistono in due varianti — una che usa l'ordinamento naturale, una che accetta un Comparator:

OperazioneOverload con ordinamento naturaleOverload con Comparator
Ordina una listaCollections.sort(list)Collections.sort(list, cmp)
Ordina una lista (moderno)list.sort(null)list.sort(cmp)
Ordina uno streamstream.sorted()stream.sorted(cmp)
Set ad alberonew TreeSet<>()new TreeSet<>(cmp)
Map ad alberonew TreeMap<>()new TreeMap<>(cmp)
Min/maxCollections.min(list)Collections.min(list, cmp)
Ricerca binariaCollections.binarySearch(list, key)Collections.binarySearch(list, key, cmp)
PriorityQueueordinamento naturale del tipo elementoil costruttore accetta un Comparator

Le forme con ordinamento naturale richiedono che il tipo elemento implementi Comparable. Se il tuo tipo non lo fa e le chiami comunque, riceverai una ClassCastException a runtime — non un errore di compilazione — perché il cast avviene all'interno dell'implementazione dell'ordinamento.

Un esempio completo: ordine naturale, comparatori personalizzati, chiavi concatenate, null

Il programma seguente definisce un record con un ordine naturale (Comparable) più tre ordinamenti esterni: per una singola chiave, per chiavi concatenate con un secondario invertito, e uno che tollera voci null.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • L'implementazione di Comparable ha ordinato per nome e ha risolto i pareggi di nome per età. Non è stato necessario alcun comparatore esplicito — l'ordinamento naturale è il predefinito per Collections.sort e simili.
  • Comparator.comparingDouble(Person::salary) è più corto e veloce rispetto a scrivere (a, b) -> Double.compare(a.salary(), b.salary()) perché evita il boxing.
  • Il comparatore concatenato ha ordinato principalmente per età e ha usato reversed() solo sulla parte salary — questo è il pattern corretto quando si vogliono direzioni diverse su chiavi diverse. Da confrontare con la chiamata di .reversed() sull'intera catena, che invertirebbe entrambe le chiavi.
  • nullsFirst ha permesso al comparatore di gestire una lista contenente voci null senza una NullPointerException. Senza quel wrapper, il primo confronto che coinvolge un null avrebbe provocato un crash.
  • Il "trucco della sottrazione" ha prodotto la risposta sbagliata per Integer.MAX_VALUE - (-1): quel calcolo va in overflow diventando un numero negativo, quindi bad riporta MAX_VALUE come minore di -1. Integer.compare produce il segno corretto ogni volta. Preferirlo sempre.

Cosa c'è dopo

Ora hai coperto iterazione (Iterator / ListIterator) e ordinamento (Comparable / Comparator). Il prossimo capitolo li unisce nella classe di utilità java.util.Collections — la cassetta degli attrezzi statica di sort, search, reverse, shuffle, min, max e metodi "avvolgi questa collezione come immutabile" che operano su qualsiasi List, Set o Map. Dopo di ciò, due brevi capitoli approfondiscono specificamente ordinamento e ricerca.

Esercizi

Pratica
Scrivi `list.sort((a, b) -> a.scoreDifference(b))` dove `scoreDifference` restituisce `a.score - b.score` come `int`. La lista contiene punteggi inclusi `Integer.MAX_VALUE` e `Integer.MIN_VALUE`, e il risultato è chiaramente sbagliato. Qual è la soluzione?
Scrivi `list.sort((a, b) -> a.scoreDifference(b))` dove `scoreDifference` restituisce `a.score - b.score` come `int`. La lista contiene punteggi inclusi `Integer.MAX_VALUE` e `Integer.MIN_VALUE`, e il risultato è chiaramente sbagliato. Qual è la soluzione?
Was this page helpful?