1. Il problema del confronto tra oggetti
Promemoria: confronto dei riferimenti e confronto degli oggetti
Sapete già che in Java l’operatore ==, quando lavora con gli oggetti, confronta i loro riferimenti — cioè se si trovano allo stesso indirizzo di memoria. Due oggetti con gli stessi campi, ma creati con new, saranno diversi per ==.
Person p1 = new Person("Sasha", 20);
Person p2 = new Person("Sasha", 20);
System.out.println(p1 == p2); // false — sono oggetti diversi in memoria!
E se vogliamo sapere se sono uguali per contenuto, usiamo equals(), hashCode() ... beh, lo sapete già. E se invece dobbiamo capire chi è «più grande», «più piccolo», «più in alto in ordine alfabetico»? Per esempio, per ordinare un elenco di utenti per età o per nome.
La necessità di ordinare e cercare oggetti
Supponiamo di avere un elenco di utenti e di volerli ordinare per età:
List<Person> people = new ArrayList<>();
people.add(new Person("Vasya", 25));
people.add(new Person("Petya", 20));
people.add(new Person("Katya", 30));
// Come ordinare?
Collections.sort(people); // Ops! E Java non sa come confrontare Person!
Il compilatore si lamenterà subito: la classe Person non implementa l’interfaccia Comparable. Java non legge nel pensiero e non sa che cosa significhi per noi «maggiore» o «minore» per Person. Per insegnarglielo, dobbiamo descrivere esplicitamente le regole di confronto.
2. L’interfaccia Comparable
Dichiarazione dell’interfaccia
L’interfaccia Comparable è il modo standard per dire a Java: «La mia classe è confrontabile, ed ecco come farlo».
public interface Comparable<T> {
int compareTo(T o);
}
Il vecchio amico a.compareTo(b) restituirà:
- un numero negativo — significa che a è «minore» di b.
- Se 0 — gli oggetti sono considerati uguali.
- un numero positivo — a è «maggiore» di b.
Esempio: implementazione di compareTo per la classe Person
Creiamo una classe Person confrontabile per età:
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getter (per gli esempi seguenti)
public String getName() { return name; }
public int getAge() { return age; }
// Implementazione del metodo compareTo
@Override
public int compareTo(Person other) {
// Ordinamento per età (crescente)
return Integer.compare(this.age, other.age);
// Alternativa: return this.age - other.age;
}
}
Punto importante: se volete ordinare in ordine decrescente, basta invertire gli argomenti: Integer.compare(other.age, this.age).
Analogia. compareTo è come un giudice in una gara: deve decidere chiaramente chi è davanti, chi è dietro e chi è allo stesso livello. Se tutti i giudici (i metodi compareTo) giudicano in modo diverso — inizierà il caos!
3. Uso di Comparable
Ordinare le collezioni con Comparable
Ora che la nostra classe implementa Comparable, l’ordinamento funziona «out of the box»:
List<Person> people = new ArrayList<>();
people.add(new Person("Vasya", 25));
people.add(new Person("Petya", 20));
people.add(new Person("Katya", 30));
Collections.sort(people); // Usa compareTo!
for (Person p : people) {
System.out.println(p.getName() + " (" + p.getAge() + ")");
}
// Petya (20)
// Vasya (25)
// Katya (30)
In modo analogo funziona anche il metodo sort della lista:
people.sort(null); // Se si passa null, viene usato compareTo
Ordinamento per nome
Se vogliamo ordinare per nome — cambiamo l’implementazione:
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
Ordinamento per più campi
A volte bisogna confrontare prima per un campo e, in caso di parità, per un altro:
@Override
public int compareTo(Person other) {
int cmp = Integer.compare(this.age, other.age);
if (cmp != 0) return cmp;
return this.name.compareTo(other.name);
}
4. Best practice nell’implementazione di Comparable
Rispettate il contratto di Comparable
- Se a.compareTo(b) == 0, allora b.compareTo(a) deve essere necessariamente 0.
- Se a.compareTo(b) < 0, allora b.compareTo(a) deve essere > 0 (e viceversa).
- Se a.compareTo(b) == 0, è auspicabile che a.equals(b) sia true (ma non è strettamente obbligatorio).
Perché è importante?
Le collezioni (per esempio, TreeSet, TreeMap) e i metodi di ordinamento possono comportarsi in modo imprevedibile se il contratto è violato. Per esempio, possono comparire «duplicati» in una collezione in cui non dovrebbero esserci.
Non dimenticate equals e hashCode
Se implementate compareTo, chiedetevi: sono implementati correttamente anche equals e hashCode? Soprattutto se la vostra classe verrà usata in collezioni come HashSet o Map.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person other = (Person) o;
return age == other.age && Objects.equals(name, other.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
Non usate in compareTo campi che possono essere null senza controlli!
Se un campo può essere null, usate confronti sicuri:
@Override
public int compareTo(Person other) {
return Objects.compare(this.name, other.name, Comparator.nullsFirst(String::compareTo));
}
Non modificate i campi coinvolti in compareTo se l’oggetto è già in una collezione ordinata
Questo può far «perdere» l’oggetto all’interno della collezione — per esempio, in TreeSet o TreeMap.
5. Sviluppiamo l’app didattica: ordinare gli utenti
Passo 1: definiamo la classe
public class Person implements Comparable<Person> {
private String name;
private int age;
// ... costruttore, getter, compareTo, equals, hashCode ...
}
Passo 2: aggiungiamo gli utenti
List<Person> people = new ArrayList<>();
people.add(new Person("Masha", 23));
people.add(new Person("Grisha", 19));
people.add(new Person("Anya", 25));
Passo 3: ordiniamo e stampiamo
Collections.sort(people);
for (Person p : people) {
System.out.println(p.getName() + " (" + p.getAge() + ")");
}
Risultato:
Grisha (19)
Masha (23)
Anya (25)
6. Schema di funzionamento di Comparable
┌────────────────────────────┐
│ La vostra classe (Person)│
├────────────────────────────┤
│ implements Comparable │
│ ↓ │
│ public int compareTo(T o) │
│ ↓ │
│ (this < o) → -1 │
│ (this == o) → 0 │
│ (this > o) → 1 │
└────────────────────────────┘
│
▼
Collections.sort(list)
│
▼
L'ordinamento funziona!
7. Dettagli utili
Come funziona Collections.sort
- Se la lista contiene oggetti che implementano Comparable, l’ordinamento userà il loro metodo compareTo.
- Se non lo implementano — si avrà un errore di compilazione.
- Per i tipi standard (Integer, String e altri) Comparable è già implementato.
Si possono avere più modi di confronto?
- In una classe — un solo «ordinamento naturale» tramite Comparable.
- Per ordini alternativi usate Comparator (lezione successiva).
Esempio: compareTo per le stringhe
String a = "apple";
String b = "banana";
System.out.println(a.compareTo(b)); // numero negativo, perché "apple" < "banana"
Tabella: cosa restituisce compareTo
| Confronto | Valore restituito |
|---|---|
|
|
|
|
|
|
8. Errori tipici nell’implementazione di Comparable
Errore n. 1: violazione del contratto di compareTo.
Se a.compareTo(b) restituisce 0, ma b.compareTo(a) non è 0, le collezioni si comporteranno in modo strano. Per esempio, TreeSet può considerare gli oggetti diversi e aggiungerli entrambi.
Errore n. 2: utilizzo di campi non inizializzati (null).
Se il campo con cui confrontate può essere null e non fate il controllo — otterrete una NullPointerException.
Errore n. 3: incoerenza tra compareTo ed equals.
Se compareTo dice che gli oggetti sono uguali (0), ma equals dice che sono diversi (false), questo porterà a bug quando si lavora con le collezioni.
Errore n. 4: modifica dei campi coinvolti in compareTo dopo l’aggiunta a una collezione ordinata.
È come cambiare il cognome sul passaporto mentre siete già in una fila in ordine alfabetico. La collezione può «perdere» il vostro oggetto.
Errore n. 5: restituire solo -1, 0 o 1.
Il metodo compareTo può restituire qualsiasi numero negativo o positivo, non necessariamente proprio -1 o 1. Ma per semplicità si usano spesso -1/0/1.
GO TO FULL VERSION