CodeGym /Corsi /JAVA 25 SELF /Interfacce della libreria standard: Comparable, Serializa...

Interfacce della libreria standard: Comparable, Serializable e altro

JAVA 25 SELF
Livello 21 , Lezione 4
Disponibile

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
Comparable<T>
Ordine naturale degli oggetti
int compareTo(T o)
Ordinamento di liste
Comparator<T>
Confronto personalizzato degli oggetti
int compare(T a, T b)
Ordinamento secondo regole diverse
Serializable
Serializzazione degli oggetti — (marker) Salvataggio/caricamento di oggetti
Cloneable
Clonazione degli oggetti — (marker) Creazione di copie degli oggetti
AutoCloseable
Chiusura automatica delle risorse
void close()
try-with-resources
Iterable<T>
Iterazione degli elementi nelle collection
Iterator<T> iterator()
ciclo for-each
Observer / EventListener Reazione agli eventi
update(), actionPerformed
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.

1
Sondaggio/quiz
Interfacce avanzate, livello 21, lezione 4
Non disponibile
Interfacce avanzate
Interfacce avanzate e interfacce funzionali
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION