Skip to content

24. Comparable, Comparator & Ordinamento in Java

Indice


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 point Unicode): 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 negativothis < other
  • Restituisce zerothis == other
  • Restituisce positivothis > other

Important

  • L’ordinamento naturale deve essere consistente con equals(), a meno che non sia esplicitamente documentato diversamente:
  • compareTo() è consistente con equals() se, e solo se, a.compareTo(b) == 0 e a.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, Comparator 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, Comparator 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.


  • Usare Comparable per l’ordinamento naturale (1 ordine predefinito).
  • Usare Comparator per 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.