1. Introduzione
Nella vita è raro che un unico modo di confrontare gli oggetti sia sufficiente. Immaginate di avere un elenco di utenti: a volte volete ordinarli per nome, a volte — per età, e a volte — per lunghezza del cognome. Oppure avete una classe di cui non siete voi gli autori e non potete aggiungervi compareTo. Proprio per questi casi in Java esiste l’interfaccia Comparator.
Quando Comparable non basta
- La classe non si può modificare (ad esempio, proviene da una libreria di terze parti).
- Servono più criteri di ordinamento (per campi diversi).
- Volete separare la logica di confronto dalla classe stessa (ad esempio, ordinare in modo diverso in parti diverse del programma).
Analogia
Se Comparable è l’«ordine naturale» integrato dell’oggetto, allora Comparator è un arbitro esterno che può valutare i vostri oggetti secondo qualsiasi criterio: oggi per nome, domani — per età, dopodomani — per lunghezza del nome.
2. Interfaccia Comparator: sintassi e contratto
Dichiarazione dell’interfaccia
public interface Comparator<T> {
int compare(T o1, T o2);
}
Il metodo compare deve restituire:
- Un numero negativo se il primo oggetto è «minore» del secondo.
- 0 se sono uguali.
- Un numero positivo se il primo è «maggiore» del secondo.
Il contratto è lo stesso di Comparable, solo che ora si confrontano due oggetti e non l’«oggetto corrente» e «l’altro» tramite compareTo.
Esempio: un Comparator per ordinare per cognome
Supponiamo di avere una classe Person:
public class Person {
private String firstName;
private String lastName;
private int age;
// Costruttore e getter
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
}
Creiamo un comparator che ordina per cognome:
import java.util.Comparator;
public class LastNameComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return a.getLastName().compareTo(b.getLastName());
}
}
Nota: il metodo compareTo delle stringhe (String) le confronta in ordine alfabetico.
3. Uso di Comparator: ordinamento delle collezioni
Ordinare con un Comparator
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Anna", "Kostetskaya", 25));
people.add(new Person("Boris", "Novak", 20));
people.add(new Person("Viktoriya", "Bell", 22));
// Ordinamento per cognome
Collections.sort(people, new LastNameComparator());
for (Person p : people) {
System.out.println(p.getLastName() + " " + p.getFirstName());
}
}
}
Risultato:
Novak Boris
Kostetskaya Anna
Bell Viktoriya
Ordinamento per età con un Comparator
Anche se la classe implementa già Comparable per il nome, si può creare un comparator separato — per l’età:
public class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return Integer.compare(a.getAge(), b.getAge());
}
}
E usarlo allo stesso modo:
Collections.sort(people, new AgeComparator());
Risultato:
Boris Novak (20)
Viktoriya Bell (22)
Anna Kostetskaya (25)
Esempio: scelta del Comparator «al volo»
Collections.sort(people, new LastNameComparator()); // Per cognome
Collections.sort(people, new AgeComparator()); // Per età
4. Classi anonime e lambda
I comparator si possono creare «al volo», senza dichiarare classi separate.
Classe anonima
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person a, Person b) {
return a.getFirstName().compareTo(b.getFirstName());
}
});
Espressione lambda
Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));
Oppure ancora più breve con il metodo della lista List.sort:
people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
- Le classi anonime sono il modo vecchio, verboso.
- Le lambda sono moderne e compatte.
5. Esempi: ordinamento per criteri diversi
Ordinamento per lunghezza del cognome
Comparator<Person> byLastNameLength = (a, b) ->
Integer.compare(a.getLastName().length(), b.getLastName().length());
people.sort(byLastNameLength);
Ordinamento per età, poi per nome (multi-livello)
Comparator<Person> byAgeThenName = (a, b) -> {
int cmp = Integer.compare(a.getAge(), b.getAge());
if (cmp != 0) return cmp;
return a.getFirstName().compareTo(b.getFirstName());
};
people.sort(byAgeThenName);
Uso di un Comparator per la ricerca (esempio)
Il comparator è utile non solo per l’ordinamento, ma anche per la ricerca in collezioni ordinate:
// people deve essere ordinato per età!
Person key = new Person("?", "?", 22);
int idx = Collections.binarySearch(people, key, new AgeComparator());
if (idx >= 0) {
System.out.println("Trovata persona con età 22: " + people.get(idx));
}
6. Best practice e particolarità di lavoro con Comparator
Non infrangete il contratto
- Se compare(a, b) restituisce 0, allora anche compare(b, a) deve restituire 0.
- Se compare(a, b) > 0, allora compare(b, a) < 0.
- Tenete conto dei possibili valori null (vedi sotto).
Non dimenticate equals e hashCode
Sebbene i comparator confrontino gli oggetti «a modo loro», per strutture come TreeSet o quando si cercano chiavi in TreeMap è importante che la logica di confronto del comparator sia coerente con equals. Altrimenti si possono ottenere risultati inaspettati: due oggetti diversi sono considerati uguali dal comparator ma non uguali secondo equals.
Ordinamento con gestione di null
Se i campi possono essere null, usate gli helper «pronti»:
Comparator<Person> byLastNameNullSafe = Comparator.comparing(
Person::getLastName,
Comparator.nullsLast(String::compareTo)
);
people.sort(byLastNameNullSafe);
7. Dettagli utili
Tabella: confronto tra Comparable e Comparator
| Comparable | Comparator | |
|---|---|---|
| Dove si implementa? | Nella classe stessa | In una classe/lambda separata |
| Metodo | |
|
| Quante varianti? | Solo uno «naturale» | Quante se ne vogliono, per ogni esigenza |
| Utilizzo | |
|
| Si può per classi di terzi? | No | Sì |
Esempio: ordinamento decrescente
Si può invertire l’ordine manualmente:
Comparator<Person> byAgeDesc = (a, b) -> Integer.compare(b.getAge(), a.getAge());
people.sort(byAgeDesc);
Oppure con reversed():
Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
people.sort(byAge.reversed());
8. Errori tipici nell’uso di Comparator
Errore n. 1: violazione del contratto di confronto. Se dimenticate che compare(a, b) e compare(b, a) devono avere segni opposti, o restituite valori arbitrari (ad esempio, semplicemente la differenza — a.getAge() - b.getAge(), che può andare in overflow), il risultato sarà imprevedibile. Usate Integer.compare e non la sottrazione: è più sicuro.
Errore n. 2: ignorare i valori null. Se i campi in base ai quali confrontate possono essere null, trattate obbligatoriamente questo caso (ad esempio con Comparator.nullsFirst/Comparator.nullsLast), altrimenti è facile ottenere una NullPointerException nel momento meno opportuno.
Errore n. 3: criteri di ordinamento instabili. Se il comparator restituisce valori diversi per gli stessi oggetti (ad esempio usa un numero casuale o un campo altamente mutevole), l’ordinamento può comportarsi in modo caotico.
Errore n. 4: incoerenza con equals. Se compare(a, b) == 0, ma a.equals(b) è false, collezioni come TreeSet e TreeMap potrebbero non funzionare come vi aspettate. È preferibile che l’uguaglianza secondo il comparator e secondo equals coincidano.
Errore n. 5: ordinare senza Comparator per classi di terzi. Se provate a ordinare oggetti di una «classe altrui» senza Comparable e senza fornire un Comparator, otterrete un errore di compilazione. Passate un comparator esplicito.
GO TO FULL VERSION