1. Interfaccia Comparable<T>
Avete mai ordinato un elenco di numeri o stringhe? Certo che sì! Ora immaginate di avere un elenco di oggetti propri — per esempio, studenti, prodotti o gattini. Come farà Java a capire in quale ordine ordinarli? Proprio per questo serve l’interfaccia Comparable<T>.
Questa interfaccia definisce l’«ordine naturale» degli oggetti — ossia l’ordine che ha senso per quel tipo di dato. Per esempio, per i numeri l’ordine crescente, per le stringhe l’ordine alfabetico, per gli studenti per cognome o per età (a vostra scelta).
Com’è strutturato Comparable
L’interfaccia è molto semplice: contiene un solo metodo:
public interface Comparable<T> {
int compareTo(T o);
}
Il metodo compareTo deve restituire:
- un numero negativo se l’oggetto corrente è «minore» dell’altro;
- 0 se sono «uguali»;
- un numero positivo se è «maggiore».
Esempio: ordiniamo gli studenti per età
Aggiungiamo la classe Student e implementiamo per essa l’interfaccia Comparable<Student>:
public class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// Getter per l'esempio
public String getName() { return name; }
public int getAge() { return age; }
@Override
public int compareTo(Student other) {
// Ordiniamo per età (crescente)
return Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
Ora possiamo ordinare facilmente un array o una lista di studenti:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("Vasya", 20));
students.add(new Student("Petya", 18));
students.add(new Student("Masha", 22));
Collections.sort(students); // Funziona grazie a Comparable!
System.out.println("Studenti ordinati:");
for (Student s : students) {
System.out.println(s);
}
}
}
Risultato:
Petya (18)
Vasya (20)
Masha (22)
Un aspetto importante
Se implementate Comparable, cercate di fare in modo che compareTo sia coerente con equals. Cioè, se a.compareTo(b) == 0, allora a.equals(b) dovrebbe essere true. Altrimenti l’ordinamento e le collection possono comportarsi in modo imprevedibile, e avrete motivo di filosofeggiare sul senso della vita del programmatore.
2. Interfaccia Serializable
La serializzazione è la capacità di un oggetto di trasformarsi in una sequenza di byte (per esempio per salvarsi su file o inviarlo in rete) e poi ricostruirsi. Immaginate di voler salvare lo stato del vostro gioco o inviare un oggetto a un server — senza serializzazione non si può.
In Java esiste a questo scopo l’interfaccia marker Serializable. Si dice «marker» perché non contiene metodi: serve solo a «marcare» la classe come serializzabile.
import java.io.Serializable;
public class Student implements Serializable {
private String name;
private int age;
// ... resto del codice
}
Come serializzare un oggetto
Per serializzazione e deserializzazione si usano le classi ObjectOutputStream e ObjectInputStream. Esempio — salviamo l’oggetto su file e lo rileggiamo:
import java.io.*;
public class Main {
public static void main(String[] args) throws Exception {
Student s = new Student("Katya", 19);
// Salviamo l'oggetto su file
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("student.dat"))) {
out.writeObject(s);
}
// Leggiamo l'oggetto dal file
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("student.dat"))) {
Student loaded = (Student) in.readObject();
System.out.println("Caricato: " + loaded);
}
}
}
Nota: Tutti i campi dell’oggetto (e degli oggetti annidati) devono essere serializzabili, altrimenti si verificherà un errore.
Perché serve un’interfaccia marker
L’interfaccia Serializable non richiede l’implementazione di metodi — comunica semplicemente alla JVM: «questo oggetto può essere serializzato». Se vi dimenticate di implementarla, il tentativo di serializzazione porterà all’eccezione NotSerializableException.
3. Altre interfacce importanti della libreria standard
Interfaccia Cloneable
Un’altra interfaccia marker. Il suo scopo è far capire alla JVM che l’oggetto può essere clonato tramite il metodo Object.clone(). Senza di essa il tentativo di chiamare clone() lancerà un’eccezione.
Tuttavia, la clonazione in Java è insidiosa. Di default è superficiale (shallow copy) e spesso è meglio scrivere metodi di copia ad hoc.
public class Student implements Cloneable {
private String name;
private int age;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
Interfaccia AutoCloseable
Questa interfaccia contiene un solo metodo close(). Qualsiasi classe che la implementa può essere usata nella costruzione try-with-resources — chiusura automatica delle risorse (ad esempio file, stream):
public class MyResource implements AutoCloseable {
@Override
public void close() {
System.out.println("Risorsa chiusa!");
}
}
public class Main {
public static void main(String[] args) {
try (MyResource res = new MyResource()) {
System.out.println("Lavoriamo con la risorsa");
}
// Qui res.close() verrà chiamato automaticamente
}
}
Interfaccia Iterable<T>
Questa interfaccia consente al vostro oggetto di essere «iterabile» nel ciclo for-each. Contiene un solo metodo iterator(), che restituisce un oggetto Iterator<T>.
public class MyList implements Iterable<String> {
// ... archivio interno
@Override
public java.util.Iterator<String> iterator() {
// Restituiamo un iteratore per scorrere gli elementi
return ...;
}
}
Tutte le collection standard (ArrayList, HashSet, ecc.) implementano Iterable, quindi possono essere iterate in for-each.
Interfaccia Comparator<T>
Questa interfaccia permette di confrontare gli oggetti secondo regole diverse, senza modificarli. Per esempio, ordinare gli studenti per nome invece che per età.
import java.util.Comparator;
Comparator<Student> byName = new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
return a.getName().compareTo(b.getName());
}
};
Nella Java moderna si fa di solito con le espressioni lambda:
Comparator<Student> byName = (a, b) -> a.getName().compareTo(b.getName());
Observer, EventListener
Queste interfacce si usano per i pattern «osservatore» e «listener di eventi» — quando un oggetto reagisce agli eventi che accadono in un altro. Per esempio, nelle interfacce grafiche (Swing, JavaFX) i gestori dei pulsanti implementano l’interfaccia ActionListener.
4. Pratica: implementiamo Comparable e serializziamo un oggetto
Esempio 1. Comparable per una classe personalizzata
Scriviamo la classe Book, ordinabile per anno di pubblicazione:
public class Book implements Comparable<Book> {
private String title;
private int year;
public Book(String title, int year) {
this.title = title;
this.year = year;
}
@Override
public int compareTo(Book other) {
return Integer.compare(this.year, other.year);
}
@Override
public String toString() {
return title + " (" + year + ")";
}
}
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Book> books = Arrays.asList(
new Book("Java dlya chaynikov", 2018),
new Book("Voyna i mir", 1869),
new Book("Garri Potter", 1997)
);
Collections.sort(books);
System.out.println(books);
}
}
Risultato:
[Voyna i mir (1869), Garri Potter (1997), Java dlya chaynikov (2018)]
Esempio 2. Serializzazione di un oggetto
import java.io.*;
public class Book implements Serializable {
private String title;
private int year;
// ... costruttore, getter, toString
public Book(String title, int year) {
this.title = title;
this.year = year;
}
@Override
public String toString() {
return title + " (" + year + ")";
}
}
public class Main {
public static void main(String[] args) throws Exception {
Book book = new Book("Java dlya chaynikov", 2018);
// Salviamo l'oggetto su file
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.dat"))) {
out.writeObject(book);
}
// Leggiamo l'oggetto dal file
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.dat"))) {
Book loaded = (Book) in.readObject();
System.out.println("Caricato: " + loaded);
}
}
}
5. Tabella: principali interfacce della libreria standard
| Interfaccia | Scopo | Metodi chiave | Esempio d’uso |
|---|---|---|---|
|
Ordine naturale degli oggetti | |
Ordinamento di liste |
|
Confronto personalizzato degli oggetti | |
Ordinamento secondo regole diverse |
|
Serializzazione degli oggetti | — (marker) | Salvataggio/caricamento di oggetti |
|
Clonazione degli oggetti | — (marker) | Creazione di copie degli oggetti |
|
Chiusura automatica delle risorse | |
try-with-resources |
|
Iterazione degli elementi nelle collection | |
ciclo for-each |
| Observer / EventListener | Reazione agli eventi | |
Gestione degli eventi nelle UI, pattern |
6. Errori tipici nell’uso delle interfacce standard
Errore n. 1: Interfaccia non implementata, ma funzionalità richiesta.
Per esempio, vi siete dimenticati di implementare Serializable ma provate a serializzare un oggetto — otterrete NotSerializableException. Analogamente con Cloneable e la chiamata a clone().
Errore n. 2: Violazione del contratto tra Comparable ed equals.
Se a.compareTo(b) == 0, ma non vale a.equals(b), le collection possono comportarsi in modo strano. Per esempio, TreeSet può «perdere» oggetti.
Errore n. 3: Copia superficiale durante la clonazione.
Il metodo clone() di default copia solo il «primo livello» dell’oggetto. Se avete campi che sono riferimenti ad altri oggetti, non vengono copiati in profondità. Questo può portare a bug misteriosi.
Errore n. 4: Non usare try-with-resources.
Se una classe implementa AutoCloseable ma non la usate con try-with-resources, rischiate di dimenticare di chiudere la risorsa — con conseguenti memory leak o blocchi di file.
Errore n. 5: Implementazione errata di compareTo o compare.
Se restituite solo 0 o 1, invece di un numero negativo/zero/positivo, l’ordinamento non funzionerà correttamente.
GO TO FULL VERSION