CodeGym /Corsi /JAVA 25 SELF /List.of, Set.of, Map.of — collezioni immutabili

List.of, Set.of, Map.of — collezioni immutabili

JAVA 25 SELF
Livello 34 , Lezione 0
Disponibile

1. Introduzione

Collezioni classiche: flessibilità e insidie

Quando si crea una raccolta con new ArrayList<>(), si ottiene una struttura che si può liberamente modificare: aggiungere, eliminare, cambiare elementi. Questo è comodo quando si costruiscono i dati «al volo». Ma cosa succede se si passa questa raccolta a un’altra classe o metodo, dove non dovrebbe essere modificata? E se la si espone per errore all’esterno e qualcuno la modifica? Ecco dove iniziano i problemi.

Esempio di errore classico

import java.util.*;

public class Example {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("Alice");
        names.add("Bob");
        names.add("Charlie");

        // Passiamo la raccolta "all'esterno"
        processNames(names);

        // Ci aspettiamo che la lista non sia cambiata...
        System.out.println(names);
    }

    public static void processNames(List<String> list) {
        // Qualcuno ha preso e rimosso un elemento!
        list.remove("Bob");
    }
}

Output:

[Alice, Charlie]

La tua raccolta è cambiata, anche se non lo avevi affatto pianificato. Nei progetti grandi tali «sorprese» si trasformano facilmente in bug estremamente spiacevoli e difficili da individuare.

Il pericolo è che il codice che lavora con la raccolta possa imbattersi all’improvviso in modifiche imprevedibili dei dati. Inoltre c’è sempre il rischio di perdita di informazioni — qualcuno ha eliminato o sovrascritto un elemento per sbaglio. E se la raccolta viene modificata contemporaneamente da thread diversi, si può incorrere non solo in ConcurrentModificationException, ma anche in un problema ancora più insidioso — dati incoerenti.

Protezione delle raccolte: approccio tradizionale

Fino a Java 9 si dovevano usare metodi wrapper come Collections.unmodifiableList(...), di cui abbiamo parlato nella lezione precedente. Aiutano a restituire una raccolta «congelata». Ma questo approccio non è sempre comodo e non risolve tutti i problemi (ne parleremo più in dettaglio nella prossima lezione).

2. Soluzione moderna: metodi factory List.of, Set.of, Map.of

In Java 9 sono stati introdotti nuovi metodi statici nelle interfacce delle collezioni: List.of, Set.of, Map.of. Consentono di creare in modo rapido e comodo una raccolta che non si può modificare. È come se avessi creato la raccolta e l’avessi subito fissata nel cemento — nessuno potrà aggiungere, eliminare o cambiare elementi.

Esempio di creazione di collezioni immutabili

import java.util.*;

public class ImmutableDemo {
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob", "Charlie");
        Set<Integer> numbers = Set.of(1, 2, 3);
        Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25, "Charlie", 28);

        System.out.println(names);
        System.out.println(numbers);
        System.out.println(ages);
    }
}

Output:

[Alice, Bob, Charlie]
[1, 2, 3]
{Alice=30, Bob=25, Charlie=28}

Come funziona?

  • List.of(...) — crea una lista immutabile.
  • Set.of(...) — crea un set (insieme) immutabile.
  • Map.of(...) — crea una mappa immutabile (fino a 10 coppie chiave-valore; per un numero maggiore usare Map.ofEntries(...)).

Attenzione! Le collezioni create con questi metodi non consentono modifiche. Qualsiasi tentativo di aggiungere, eliminare o sostituire un elemento genererà un’eccezione.

3. Esempi d’uso e «insidie»

Esempio: tentativo di modificare una collezione

import java.util.*;

public class ImmutableFail {
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob");
        // names.add("Charlie"); // Errore a runtime!
        try {
            names.add("Charlie");
        } catch (UnsupportedOperationException ex) {
            System.out.println("Impossibile aggiungere l'elemento: " + ex.getClass().getSimpleName());
        }
    }
}

Output:

Impossibile aggiungere l'elemento: UnsupportedOperationException

Esempio: tentativo di aggiungere null

import java.util.*;

public class NullFail {
    public static void main(String[] args) {
        try {
            List<String> badList = List.of("Alice", null, "Bob");
        } catch (NullPointerException ex) {
            System.out.println("Null non consentito: " + ex.getClass().getSimpleName());
        }
    }
}

Output:

Null non consentito: NullPointerException

Esempio: duplicati in Set.of

import java.util.*;

public class DuplicatesFail {
    public static void main(String[] args) {
        try {
            Set<String> badSet = Set.of("one", "two", "one");
        } catch (IllegalArgumentException ex) {
            System.out.println("Duplicati non consentiti: " + ex.getClass().getSimpleName());
        }
    }
}

Output:

Duplicati non consentiti: IllegalArgumentException

Esempio: Map.of con un numero elevato di coppie

import java.util.*;

public class MapOfLarge {
    public static void main(String[] args) {
        // Map.of supporta fino a 10 coppie chiave-valore
        Map<String, Integer> map = Map.of(
            "one", 1, "two", 2, "three", 3, "four", 4, "five", 5,
            "six", 6, "seven", 7, "eight", 8, "nine", 9, "ten", 10
        );
        System.out.println(map);

        // Per un numero maggiore usare Map.ofEntries
        Map<String, Integer> bigMap = Map.ofEntries(
            Map.entry("eleven", 11),
            Map.entry("twelve", 12),
            Map.entry("thirteen", 13)
            // ...e così via
        );
        System.out.println(bigMap);
    }
}

4. Caratteristiche e limitazioni delle collezioni immutabili

Impossibile modificare.
Qualsiasi tentativo di aggiungere, eliminare o modificare un elemento porterà a UnsupportedOperationException. Anche i metodi che di solito sono consentiti (add, remove, set) non funzionano.

Non è possibile usare null.
Se si prova ad aggiungere null come elemento di una lista o di un set, o come chiave/valore in una mappa, si otterrà NullPointerException. È fatto per sicurezza: gli elementi null spesso portano a errori nelle collezioni.

Nessuna implementazione specifica è garantita.
Non saprete quale classe concreta sta sotto il cofano della collezione creata tramite List.of e simili. Non conviene fare instanceof su ArrayList o tentare cast a un tipo specifico.

Ordine degli elementi.
— In List.of l’ordine degli elementi è preservato (come in una lista normale).
— In Set.of l’ordine non è garantito (nella pratica può coincidere con l’ordine di passaggio degli argomenti, ma è meglio non farci affidamento).
— In Map.of l’ordine delle coppie non è garantito.

Prestazioni.
Le collezioni create tramite i metodi factory in genere funzionano più velocemente dei wrapper sopra collezioni mutabili, perché non sprecano memoria per funzionalità superflue.

5. Quando e perché usare collezioni immutabili

Set di dati costanti

Se avete una lista, un set o una mappa che non devono cambiare durante l’esecuzione del programma, usate List.of, Set.of, Map.of. Per esempio:

private static final List<String> ROLES = List.of("USER", "ADMIN", "MODERATOR");

Ora nessuno potrà aggiungere un ruolo superfluo a questa lista.

Restituire collezioni dai metodi

Se restituite una collezione da un metodo e non volete che qualcuno la modifichi dall’esterno:

public List<String> getDefaultNames() {
    return List.of("Alice", "Bob", "Charlie");
}

Il destinatario non potrà rovinare i vostri dati.

Passaggio tra i layer dell’applicazione

Quando si passano collezioni tra parti diverse del programma (ad esempio tra i layer Controller e Service in una web app), è meglio usare collezioni immutabili, così nessuno potrà modificarle «di nascosto».

Sicurezza e thread-safety

Le collezioni immutabili sono per definizione thread-safe in lettura: se nessuno può modificarle, si possono usare tranquillamente da più thread senza sincronizzazione.

6. Esempi pratici per un’app generica

Supponiamo che nella nostra app didattica ci sia un elenco di comandi supportati:

public class Commands {
    public static final List<String> SUPPORTED_COMMANDS = List.of(
        "help", "exit", "list", "add", "remove"
    );
}

Se provate a fare così:

Commands.SUPPORTED_COMMANDS.add("hack_the_system");

Otterrete un’eccezione e non riuscirete a danneggiare l’applicazione.

Oppure, per esempio, se avete una mappa con i codici di errore:

public class ErrorCodes {
    public static final Map<Integer, String> CODES = Map.of(
        404, "Not Found",
        500, "Internal Server Error",
        403, "Forbidden"
    );
}

Qualsiasi tentativo di aggiungere un nuovo codice genererà un’eccezione.

7. Confronto degli approcci di creazione delle collezioni

Modo di creazione Modificabile? Null consentito? Duplicati? Thread-safety Esempio
new ArrayList<>()
No
new ArrayList<>()
List.of(...)
No No Sì*
List.of("a", "b")
Set.of(...)
No No No Sì*
Set.of("a", "b")
Map.of(...)
No No No Sì*
Map.of("a", 1, "b", 2)
Collections.unmodifiableList(...)
No Dipende dall’originale No
Collections.unmodifiableList(list)

* — thread-safety solo in termini di immutabilità: se nessuno modifica la raccolta, può essere letta in modo sicuro da più thread.

8. Errori tipici nell’uso di List.of, Set.of, Map.of

Errore n. 1: tentativo di modificare la collezione.
Errore molto frequente — provare ad aggiungere o rimuovere un elemento da una collezione creata con List.of, Set.of o Map.of. Per esempio, names.add("Dmitry") o ages.remove("Bob"). Porta sempre a UnsupportedOperationException a runtime.

Errore n. 2: tentativo di aggiungere null.
Se per sbaglio passate null a uno qualsiasi dei metodi (ad esempio, List.of("Alice", null)), otterrete NullPointerException. Le collezioni immutabili di Java 9+ non amano null — e in realtà è un bene.

Errore n. 3: duplicati di elementi in Set.of o Map.of.
Set.of("a", "b", "a") o Map.of("x", 1, "x", 2) porteranno a IllegalArgumentException. Per definizione, il set e la mappa non possono contenere duplicati.

Errore n. 4: aspettarsi un’implementazione specifica.
Non conviene fare così:

List<String> list = List.of("a", "b");
if (list instanceof ArrayList) { 
    // ...
} // Questo è sempre false!

L’implementazione interna è nascosta — non fate affidamento sui dettagli di implementazione.

Errore n. 5: tentativo di usare metodi di modifica della collezione.
Anche metodi come clear(), set(index, value) (sulle liste) genereranno eccezioni. Ricordate: le collezioni create tramite metodi factory sono immutabili.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION