CodeGym /Corsi /JAVA 25 SELF /Collezioni CopyOnWrite, wrapper non modificabili

Collezioni CopyOnWrite, wrapper non modificabili

JAVA 25 SELF
Livello 34 , Lezione 2
Disponibile

1. Unmodifiable wrappers: wrapper per le collezioni

A volte nel codice c'è già una collezione che qualcuno può modificare per sbaglio (o non proprio per sbaglio). Per esempio, avete una lista di utenti che volete esporre all'esterno, ma non volete che qualcuno la modifichi:

List<String> users = new ArrayList<>();
users.add("Alice");
users.add("Bob");

Restituite questa lista da un metodo e qualcuno fa users.add("Hacker"); — ed ecco che nel sistema compare un nuovo utente non autorizzato! Come proteggersi?

Wrapper di Collections

In Java esistono da tempo metodi speciali di wrapper nella classe Collections:

  • Collections.unmodifiableList(list)
  • Collections.unmodifiableSet(set)
  • Collections.unmodifiableMap(map)

Questi metodi restituiscono un wrapper sulla vostra collezione che non consente di modificarla tramite il wrapper stesso. Un tentativo di aggiungere, rimuovere o cambiare un elemento tramite il wrapper genererà UnsupportedOperationException.

Esempio:

import java.util.*;

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

        // Creiamo un wrapper non modificabile
        List<String> unmodifiable = Collections.unmodifiableList(modifiable);

        System.out.println(unmodifiable); // [Alice, Bob]

        // Proviamo ad aggiungere un elemento tramite il wrapper
        try {
            unmodifiable.add("Charlie"); // Boom! UnsupportedOperationException
        } catch (UnsupportedOperationException e) {
            System.out.println("Impossibile modificare la collezione: " + e);
        }
    }
}

Importante!

  • Il wrapper NON rende immutabile la collezione originale. Se qualcuno mantiene un riferimento alla collezione originale, può ancora modificarla.
  • Tutte le modifiche della collezione originale sono visibili attraverso il wrapper.
modifiable.add("Charlie");
System.out.println(unmodifiable); // [Alice, Bob, Charlie]

Cioè, se da qualche parte nel codice qualcuno aggiunge/rimuove un elemento nella lista originale, il wrapper lo vedrà. Non è un «congelamento», ma solo un divieto di modifica tramite il wrapper stesso.

Wrapper per altre collezioni

Allo stesso modo si possono creare wrapper per Set, Map e persino per strutture più esotiche:

Set<Integer> numbers = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> unmodSet = Collections.unmodifiableSet(numbers);

Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
Map<String, Integer> unmodMap = Collections.unmodifiableMap(ages);

Confronto con i metodi di factory (List.of e altri)

  • List.of(...) crea una nuova collezione immutabile alla quale non si può aggiungere nulla fin dall’inizio.
  • Collections.unmodifiableList(list) — è un wrapper sopra una collezione esistente. Se la lista originale cambia, cambia anche il wrapper.

Tabella: confronto degli approcci

List.of(...)
Collections.unmodifiableList(list)
Si può aggiungere? No No (tramite il wrapper)
Si può aggiungere all'originale? Non applicabile
Si vedono le modifiche? No
Si può inserire null? No (NPE) Sì (se la collezione originale lo consente)
Implementazione Propria Wrapper sopra la vostra collezione

2. Collezioni CopyOnWrite

Nelle applicazioni multithread è frequente il caso: un thread (o più) legge una collezione, e un altro (o altri) la modificano di tanto in tanto. Le collezioni normali non vanno bene: possibili race condition, errori, ConcurrentModificationException e altre «gioie» del mondo multithread.

Per questi casi sono state ideate le collezioni CopyOnWrite — create appositamente per scenari in cui le letture sono frequenti e le modifiche rare.

Come funziona?

  • A ogni modifica (aggiunta, rimozione, sostituzione) la collezione crea una nuova copia dell'array interno.
  • Tutti i thread lettori ottengono una «propria» versione dell'array, che non cambia finché la stanno leggendo.
  • Questo rende la lettura assolutamente sicura e non richiede sincronizzazione.

Classi principali

  • CopyOnWriteArrayList<E>
  • CopyOnWriteArraySet<E>

Si trovano nel package java.util.concurrent.

Esempio d'uso

import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
        cowList.add("Alpha");
        cowList.add("Beta");

        // Si può iterare in sicurezza, anche se qualcuno aggiunge elementi in parallelo
        for (String s : cowList) {
            System.out.println(s);
            cowList.add("Gamma"); // Non genererà ConcurrentModificationException!
        }

        System.out.println(cowList); // [Alpha, Beta, Gamma, Gamma]
    }
}

Caratteristiche:

  • L'iteratore delle collezioni CopyOnWrite «vede» sempre un istantanea della collezione al momento della creazione dell'iteratore.
  • Se dopo la creazione dell'iteratore qualcuno aggiunge elementi, l'iteratore non li vedrà.
  • Si possono aggiungere/rimuovere elementi in sicurezza durante l'iterazione — nessuna ConcurrentModificationException!

Quando usare le collezioni CopyOnWrite?

Sono adatte a situazioni in cui nel programma operano molti thread che principalmente leggono i dati dalla collezione, mentre le operazioni di modifica sono molto rare. Esempio classico — la lista dei listener di eventi (event listeners): nuovi listener vengono aggiunti o rimossi di rado, mentre la notifica di questi listener avviene continuamente.

Esempio — sottoscrittori di eventi

import java.util.concurrent.CopyOnWriteArrayList;

public class EventBus {
    private final CopyOnWriteArrayList<Runnable> listeners = new CopyOnWriteArrayList<>();

    public void subscribe(Runnable listener) {
        listeners.add(listener);
    }

    public void publishEvent() {
        for (Runnable listener : listeners) {
            listener.run(); // sicuro, anche se qualcuno si è iscritto/disiscritto proprio ora!
        }
    }
}

Svantaggi delle collezioni CopyOnWrite

  • Lento per modifiche frequenti: ogni modifica comporta la creazione di una nuova copia dell'array, costosa in termini di memoria e tempo.
  • Inefficiente per collezioni grandi: se la collezione è grande, copiare l'array è un'operazione costosa.

3. Confronto: quando usare cosa?

Unmodifiable wrappers (Collections.unmodifiable...)

Quando usare: quando avete già una collezione che volete proteggere dalle modifiche da codice esterno, ma le modifiche dall'interno (il proprietario della collezione) sono ammesse.

Sicurezza rispetto ai thread: non è garantita! Se la collezione originale viene modificata da un altro thread, possono verificarsi race condition ed errori.

Metodi di factory (List.of, Set.of, Map.of)

Quando usare: quando volete creare subito una collezione immutabile costante, senza possibilità di modifica da nessuna parte.

Sicurezza rispetto ai thread: garantita (la collezione non cambia affatto).

Collezioni CopyOnWrite

Quando usare: in scenari multithread con molte letture e poche modifiche. Per esempio, per le liste dei sottoscrittori.

Sicurezza rispetto ai thread: sì, completamente thread-safe.

Immutabilità: no, la collezione può essere modificata, ma ogni volta viene creata una nuova copia affinché i lettori non ne risentano.

4. Errori tipici e particolarità di implementazione

Errore n. 1: aspettarsi il «congelamento» della collezione originale tramite un wrapper. Molti pensano che Collections.unmodifiableList(list) renda la collezione completamente immutabile. In realtà, se qualcuno ha ancora un riferimento alla lista originale, può modificarla e queste modifiche saranno visibili attraverso il wrapper. Soluzione: se serve la vera immutabilità — usate List.copyOf(list) (Java 10+) oppure List.of(...).

Errore n. 2: usare CopyOnWrite per una collezione che cambia spesso. Se in CopyOnWriteArrayList si aggiungono o rimuovono elementi di continuo, ciò porterà a problemi di prestazioni e memoria. CopyOnWrite è adatto solo a scenari «molti lettori, pochi scrittori».

Errore n. 3: aspettarsi che i wrapper siano thread-safe. Collections.unmodifiableList non rende la collezione thread-safe! Se la lista originale viene modificata da thread diversi, sono possibili errori.

Errore n. 4: usare collezioni create con List.of o Set.of con null. A differenza delle collezioni normali, i metodi di factory non ammettono null — il tentativo di aggiungere o persino creare una collezione con null porterà a NullPointerException.

1
Compito
JAVA 25 SELF, livello 34, lezione 2
Bloccato
Protezione dell'elenco di parole vietate 🚫
Protezione dell'elenco di parole vietate 🚫
1
Compito
JAVA 25 SELF, livello 34, lezione 2
Bloccato
Registro eventi in tempo reale 📈
Registro eventi in tempo reale 📈
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION