24. Comparable, Comparator & Ordinamento in Java
Indice
- 24.1 Comparable — Ordinamento Naturale
- 24.2 Comparator — Ordinamento Personalizzato
- 24.3 Comparable vs Comparator
- 24.4 Ordinamento di Array e Collezioni
- 24.5 Ordinamento Multi-Livello thenComparing
- 24.6 Confrontare Primitivi in Modo Efficiente
- 24.7 Trappole Comuni
- 24.8 Esempio Completo
- 24.9 Riepilogo
Java fornisce due strategie principali per l’ordinamento e il confronto: Comparable (ordinamento naturale) e Comparator (ordinamento personalizzato).
Comprendere le loro regole, i vincoli e le interazioni con i generics è essenziale.
- Per i tipi numerici, l’ordinamento segue l’ordine numerico naturale, il che significa che i valori più piccoli vengono prima di quelli più grandi.
- L’ordinamento delle stringhe segue l’ordine lessicografico (
code pointUnicode): confronto carattere per carattere; le cifre vengono prima delle maiuscole, le maiuscole prima delle minuscole.
Questo ordinamento si basa sul code point Unicode di ogni carattere, non sull’intuizione alfabetica.
Un Unicode code point è un valore numerico unico assegnato ai caratteri nell'Unicode standard.
Più precisamente:
un Unicode code point è un integer (scritto in esadecimale come U+XXXX) che rappresenta uno specifico carattere, simbolo, o carattere speciale indipendentemente da font, lingua, o piattaforma.
- Esempi:
- U+0041 → A
- U+0061 → a
- U+0030 → 0
- U+1F600 → 😀
Un code point non è una sequenza di byte; è un numero astratto.
Come il code point sia poi stoccato nella memoria fisica dipende dall'encoding (UTF-8, UTF-16, UTF-32).
Unicode definisce code point da U+0000 a U+10FFFF.
In breve: Unicode code points definisce quale sia il carattere; l'encodings definisce come questo sia rappresentato in bytes.
- Esempi di natural ordering
List<String> items = List.of("10", "2", "A", "Z", "a", "b");
List<String> sorted = new ArrayList<>(items);
Collections.sort(sorted);
System.out.println(sorted);
Output:
[10, 2, A, Z, a, b]
Note
L’ordinamento naturale è definito solo per i tipi che implementano Comparable.
24.1 Comparable — Ordinamento Naturale
L’interfaccia Comparable<T> definisce l’ordine naturale di un tipo.
Una classe la implementa quando vuole definire la propria regola di ordinamento predefinita.
24.1.1 Contratto del Metodo di Comparable
public interface Comparable<T> {
int compareTo(T other);
}
Regole e restituzione:
- Restituisce negativo →
this<other - Restituisce zero →
this==other - Restituisce positivo →
this>other
Important
- L’ordinamento naturale deve essere consistente con
equals(), a meno che non sia esplicitamente documentato diversamente: compareTo()è consistente conequals()se, e solo se,a.compareTo(b) == 0ea.equals(b) è true.
Warning
compareTo può lanciare ClassCastException se riceve un tipo non confrontabile — ma questo di solito succede solo con tipi raw.
24.1.2 Esempio: Classe che Implementa Comparable
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String n, int a) {
this.name = n;
this.age = a;
}
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
var list = List.of(new Person("Bob", 40), new Person("Alice", 30));
list.stream().sorted().forEach(p -> System.out.println(p.getAge()));
La lista viene ordinata per età, perché quello è l’ordine numerico naturale.
24.1.3 Errori Comuni di Comparable
- Confrontare tutti i campi rilevanti → risultati inconsistenti se non lo si fa
- Violare la transitività → porta a comportamento indefinito
- Lanciare eccezioni dentro compareTo() rompe l’ordinamento
- Non implementare la stessa logica di equals() → trappola comune
24.2 Comparator — Ordinamento Personalizzato
L’interfaccia Comparator<T> consente di definire più strategie di ordinamento
senza modificare la classe stessa.
24.2.1 Metodi Principali di Comparator
int compare(T a, T b);
Metodi di supporto aggiuntivi:
24.2.1.1 Metodi di Supporto Statici di Comparator
| Metodo | Statico / Istanza | Tipo di Ritorno | Parametri | Descrizione |
|---|---|---|---|---|
Comparator.comparing(keyExtractor) |
statico | Comparator |
Function<? super T, ? extends U> | Costruisce un comparator che confronta le chiavi estratte usando l’ordinamento naturale. |
Comparator.comparing(keyExtractor, keyComparator) |
statico | Comparator |
Function |
Costruisce un comparator che confronta le chiavi estratte usando un comparator personalizzato. |
Comparator.comparingInt(keyExtractor) |
statico | Comparator |
ToIntFunction |
Comparator ottimizzato per chiavi int (evita il boxing). |
Comparator.comparingLong(keyExtractor) |
statico | Comparator |
ToLongFunction |
Comparator ottimizzato per chiavi long. |
Comparator.comparingDouble(keyExtractor) |
statico | Comparator |
ToDoubleFunction |
Comparator ottimizzato per chiavi double. |
Comparator.naturalOrder() |
statico | Comparator |
none | Comparator che usa l’ordinamento naturale (Comparable). |
Comparator.reverseOrder() |
statico | Comparator |
none | Ordinamento naturale inverso. |
Comparator.nullsFirst(comparator) |
statico | Comparator |
Comparator |
Incapsula un comparator in modo che i null vengano confrontati prima dei non-null. |
Comparator.nullsLast(comparator) |
statico | Comparator |
Comparator |
Incapsula un comparator in modo che i null vengano confrontati dopo i non-null. |
24.2.1.2 Metodi di Istanza su Comparator
| Metodo | Statico / Istanza | Tipo di Ritorno | Parametri | Descrizione |
|---|---|---|---|---|
thenComparing(otherComparator) |
istanza | Comparator |
Comparator |
Aggiunge un comparator secondario quando il primario confronta come uguali. |
thenComparing(keyExtractor) |
istanza | Comparator |
Function |
Confronto secondario usando l’ordinamento naturale della chiave estratta. |
thenComparing(keyExtractor, keyComparator) |
istanza | Comparator |
Function |
Confronto secondario con comparator personalizzato. |
thenComparingInt(keyExtractor) |
istanza | Comparator |
ToIntFunction |
Confronto numerico secondario (ottimizzato). |
thenComparingLong(keyExtractor) |
istanza | Comparator |
ToLongFunction |
Confronto numerico secondario. |
thenComparingDouble(keyExtractor) |
istanza | Comparator |
ToDoubleFunction |
Confronto numerico secondario. |
reversed() |
istanza | Comparator |
none | Restituisce un comparator invertito per la stessa logica di confronto. |
24.2.2 Esempio di Comparator
var people = List.of(new Person("Bob",40), new Person("Ann",30));
Comparator<Person> byName = Comparator.comparing(Person::getName);
Comparator<Person> byAgeDesc = Comparator.comparingInt(Person::getAge).reversed();
var sorted = people.stream().sorted(byName.thenComparing(byAgeDesc)).toList();
24.3 Comparable vs Comparator
| Caratteristica | Comparable | Comparator |
|---|---|---|
| Package | java.lang | java.util |
| Metodo | compareTo(T) | compare(T,T) |
| Tipo di Ordinamento | Naturale (predefinito) | Personalizzato (strategie multiple) |
| Modifica la Classe Sorgente | SI | NO |
| Utile Per | Ordinamento predefinito | Ordinamento esterno o alternativo |
| Consente Ordini Multipli | NO | SI |
| Usato da Collections.sort | SI | SI |
| Usato da Arrays.sort | SI | SI |
24.4 Ordinamento di Array e Collezioni
24.4.1 Arrays sort()
int[] nums = {3,1,2};
Arrays.sort(nums); // ordine naturale
Person[] arr = {...};
Arrays.sort(arr); // Person deve implementare Comparable
Arrays.sort(arr, byName); // usando Comparator
24.4.2 Collections sort()
Collections.sort(list); // ordine naturale
Collections.sort(list, byName); // comparator
Note
Collections.sort(list) delega a list.sort(comparator) da Java 8.
24.5 Ordinamento Multi-Livello (thenComparing)
var cmp = Comparator
.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
.thenComparingInt(Person::getAge);
24.6 Confrontare Primitivi in Modo Efficiente
Comparator.comparingInt(Person::getAge)
Comparator.comparingLong(...)
Comparator.comparingDouble(...)
Note
Questi evitano il boxing e sono preferiti nel codice sensibile alle prestazioni.
24.7 Trappole Comuni
- Ordinare una lista di Object senza Comparable → ClassCastException a runtime
- compareTo inconsistente con equals → comportamento imprevedibile
- Comparator che rompe la transitività → l’ordinamento diventa indefinito
- Elementi null → a meno che il Comparator li gestisca, l’ordinamento lancia NPE
- Comparator che confronta campi di tipi misti → ClassCastException
- Usare la sottrazione per confrontare int può causare overflow → usare sempre
Integer.compare() - Ordinare una lista con elementi null e ordine naturale → NPE
- compareTo non deve mai restituire negativo/zero/positivo inconsistenti sugli stessi due oggetti (niente casualità)
24.8 Esempio Completo
record Book(String title, double price, int year) {}
var books = List.of(
new Book("Java 17", 40.0, 2021),
new Book("Algorithms", 55.0, 2019),
new Book("Java 21", 42.0, 2023)
);
Comparator<Book> cmp =
Comparator
.comparingDouble(Book::price)
.thenComparing(Book::year)
.reversed();
books.stream().sorted(cmp)
.forEach(System.out::println);
Note
reversed() si applica all’intero comparator composto, non solo alla prima chiave di confronto.
24.9 Riepilogo
- Usare
Comparableper l’ordinamento naturale (1 ordine predefinito). - Usare
Comparatorper strategie di ordinamento flessibili o multiple. - I comparator possono essere composti (reversed, thenComparing).
- L’ordinamento richiede una logica di confronto consistente.
- Arrays.sort e Collections.sort usano sia Comparable che Comparator.