CodeGym /Cursos /JAVA 25 SELF /Coleções CopyOnWrite, wrappers não modificáveis

Coleções CopyOnWrite, wrappers não modificáveis

JAVA 25 SELF
Nível 34 , Lição 2
Disponível

1. Wrappers não modificáveis: invólucros para coleções

Às vezes, o código já tem uma coleção que alguém pode alterar por engano (ou não tão por engano). Por exemplo, você tem uma lista de usuários que deseja expor, mas não quer que alguém a modifique:

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

Você retorna essa lista a partir de um método, e alguém faz users.add("Hacker"); — e pronto, aparece um novo usuário não autorizado no seu sistema! Como se proteger?

Invólucros de Collections

O Java já possui há muito tempo métodos especiais de invólucro na classe Collections:

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

Esses métodos retornam um invólucro sobre sua coleção, que não permite alterá-la por meio dele. Uma tentativa de adicionar, remover ou modificar um elemento através do invólucro lançará UnsupportedOperationException.

Exemplo:

import java.util.*;

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

        // Criamos um invólucro não modificável
        List<String> unmodifiable = Collections.unmodifiableList(modifiable);

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

        // Vamos tentar adicionar um elemento pelo invólucro
        try {
            unmodifiable.add("Charlie"); // Boom! UnsupportedOperationException
        } catch (UnsupportedOperationException e) {
            System.out.println("Não é possível modificar a coleção: " + e);
        }
    }
}

Importante!

  • O invólucro NÃO torna a coleção original imutável. Se alguém mantiver uma referência à coleção original, ainda poderá alterá-la.
  • Todas as alterações na coleção original ficam visíveis através do invólucro.
modifiable.add("Charlie");
System.out.println(unmodifiable); // [Alice, Bob, Charlie]

Ou seja, se alguém em algum lugar no código adicionar/remover um elemento na lista original, o invólucro verá isso. Isso não é um “congelamento”, é apenas uma proibição de alterar através do próprio invólucro.

Invólucros para outras coleções

Da mesma forma, é possível criar invólucros para Set, Map e até mesmo para estruturas mais exóticas:

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);

Comparação com os métodos de fábrica (List.of e outros)

  • List.of(...) cria uma coleção nova e imutável, na qual não é possível adicionar nem desde o início.
  • Collections.unmodifiableList(list) — é um invólucro sobre uma coleção existente. Se a lista original mudar, o invólucro também mudará.

Tabela: comparação das abordagens

List.of(...)
Collections.unmodifiableList(list)
Pode adicionar? Não Não (pelo invólucro)
Pode adicionar na original? Não aplicável Sim
As alterações são visíveis? Não Sim
Pode colocar null? Não (NPE) Sim (se a coleção original permitir)
Implementação Própria Invólucro sobre a sua coleção

2. Coleções CopyOnWrite

Em programas multithread, muitas vezes surge a tarefa: um thread (ou vários) lê a coleção, e outro (ou outros) às vezes a modifica. Coleções comuns não servem aqui: podem ocorrer condições de corrida, erros, ConcurrentModificationException e outras “alegrias” do mundo concorrente.

Para esses casos, foram criadas as coleções CopyOnWrite — elas são projetadas para cenários em que leituras acontecem com frequência e alterações, raramente.

Como funciona?

  • A cada modificação (adição, remoção, substituição), a coleção cria uma nova cópia do array interno.
  • Todos os threads leitores recebem sua “própria” versão do array, que não muda enquanto eles leem.
  • Isso torna a leitura absolutamente segura e não requer sincronização.

Classes principais

  • CopyOnWriteArrayList<E>
  • CopyOnWriteArraySet<E>

Elas estão no pacote java.util.concurrent.

Exemplo de 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");

        // É seguro iterar, mesmo que alguém esteja adicionando elementos em paralelo
        for (String s : cowList) {
            System.out.println(s);
            cowList.add("Gamma"); // Não lançará ConcurrentModificationException!
        }

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

Características:

  • O iterador das coleções CopyOnWrite sempre “enxerga” um instantâneo da coleção no momento da criação do iterador.
  • Se, após a criação do iterador, alguém adicionar elementos, o iterador não os verá.
  • É possível adicionar/remover elementos com segurança durante a iteração — sem ConcurrentModificationException!

Quando usar coleções CopyOnWrite?

Elas são adequadas para situações em que o programa possui muitos threads que principalmente leem dados da coleção, e operações de modificação são muito raras. O exemplo clássico é a lista de ouvintes de eventos (event listeners): novos ouvintes são adicionados ou removidos raramente, mas a notificação desses ouvintes acontece constantemente.

Exemplo — assinantes de eventos

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(); // seguro, mesmo que alguém se inscreva/cancele a inscrição agora mesmo!
        }
    }
}

Desvantagens das coleções CopyOnWrite

  • Lento para alterações frequentes: cada alteração cria uma nova cópia do array, o que é custoso em memória e tempo.
  • Ineficiente para coleções grandes: se a coleção for grande, copiar o array é uma operação cara.

3. Comparação: quando usar o quê?

Wrappers não modificáveis (Collections.unmodifiable...)

Quando usar: Quando você já tem uma coleção que deseja proteger contra alterações por código externo, mas alterações internas (do proprietário da coleção) são aceitáveis.

Segurança de thread: Não é garantida! Se a coleção original for modificada por outro thread, podem ocorrer corridas e erros.

Métodos de fábrica (List.of, Set.of, Map.of)

Quando usar: Quando você quer criar uma coleção imutável constante desde o início, sem possibilidade de alteração de lugar nenhum.

Segurança de thread: É garantida (a coleção não muda de forma alguma).

Coleções CopyOnWrite

Quando usar: Em cenários multithread com muitas leituras e poucas alterações. Por exemplo, para listas de assinantes/ouvintes.

Segurança de thread: Sim, são totalmente thread-safe.

Imutabilidade: Não. A coleção pode mudar, mas a cada vez é criada uma nova cópia para que os leitores não sejam afetados.

4. Erros comuns e particularidades de implementação

Erro nº 1: esperar “congelamento” da coleção original por meio de um invólucro. Muitos pensam que Collections.unmodifiableList(list) torna a coleção totalmente imutável. Na prática, se alguém manteve uma referência para a lista original, poderá alterá-la, e essas alterações serão visíveis através do invólucro. Solução: Se você precisa de imutabilidade de verdade — use List.copyOf(list) (Java 10+) ou List.of(...).

Erro nº 2: usar CopyOnWrite para uma coleção que muda frequentemente. Se em CopyOnWriteArrayList elementos são constantemente adicionados ou removidos, isso levará a problemas de desempenho e uso de memória. CopyOnWrite é adequado apenas para cenários com “muitos leitores, poucos escritores”.

Erro nº 3: esperar que os invólucros sejam thread-safe. Collections.unmodifiableList não torna a coleção thread-safe! Se a lista original for alterada por diferentes threads, erros poderão ocorrer.

Erro nº 4: usar coleções de List.of ou Set.of com null. Ao contrário das coleções comuns, os métodos de fábrica não aceitam null — tentar adicionar ou até mesmo criar uma coleção com null resultará em NullPointerException.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION