Coleções Não Modificáveis em Java
Crie coleções imutáveis em Java com List.of, Set.of, Map.of e os wrappers Collections.unmodifiable*.
Uma coleção modificável permite que qualquer pessoa com uma referência altere seu conteúdo. Uma coleção não modificável não permite — chamar add, remove, put, clear ou set lança UnsupportedOperationException. Java tem duas formas complementares de criar uma: as factories .of(...) introduzidas no Java 9 (List.of, Set.of, Map.of, Map.ofEntries) e os mais antigos wrappers Collections.unmodifiable*. Elas parecem similares no ponto de chamada, mas se comportam de forma diferente em dois aspectos importantes, e a ferramenta certa depende do que você realmente precisa.
Este capítulo encerra a parte do framework de coleções fornecendo uma receita moderna e limpa para "me dê uma constante" e "me dê um snapshot."
Por que imutabilidade
Três ganhos concretos que justificam o padrão:
- Compartilhamento seguro. Entregue uma lista não modificável a um construtor, a uma thread de trabalho ou a um consumidor de eventos e você não precisa se preocupar com eles mutando seu estado. O tipo no nível do compilador não diz "somente leitura," mas o runtime diz.
- Hashável com segurança. Colocar uma
Listmutável em umHashSeté um bug — se o conteúdo da lista mudar, seuhashCodemuda, e o set perde o elemento. Coleções não modificáveis evitam isso completamente. - Melhor design de API. Retornar uma visão não modificável de um getter diz "isso é meu — leia, não altere." Sem isso, cada chamador precisa decidir se deve fazer uma cópia defensiva.
As duas estratégias
List.of, Set.of, Map.of, Map.ofEntries — coleções verdadeiramente imutáveis
Adicionadas no Java 9. Elas constroem uma nova coleção com seu próprio armazenamento interno. Nada mais tem uma referência para ela:
List<String> roles = List.of("admin", "editor", "viewer");
Set<Integer> primes = Set.of(2, 3, 5, 7, 11);
Map<String, Integer> ages = Map.of("alice", 30, "bob", 25);
Map<String, Integer> many = Map.ofEntries(
Map.entry("alice", 30),
Map.entry("bob", 25),
Map.entry("carol", 28)
);Use estas para constantes e literais — coleções fixas pequenas que você escreve no código. O JIT do compilador as compila em representações altamente compactas e de baixo overhead (muitas vezes um único array inline). O custo é zero por alocação além do que o próprio literal ocupa.
Três restrições para lembrar:
- Sem elementos
null, sem chavesnull, sem valoresnull.List.of("a", null)lançaNullPointerExceptionna construção. Se você precisar representar "ausente," useOptionalou omita a chave do mapa. - Sem duplicatas para
Set.ofeMap.of.Set.of("a", "a")lançaIllegalArgumentException. Elas são destinadas a dados literais que você controla. Map.oftem sobrecargas apenas até 10 entradas. Para 11 ou mais, useMap.ofEntries(Map.entry(...), Map.entry(...), ...).
Collections.unmodifiableList(coll) etc. — visões de uma coleção existente
Envolva uma coleção em uma visão somente leitura. O original ainda é mutável, e mudanças através do original são visíveis através da visão:
List<String> mutable = new ArrayList<>(List.of("a", "b", "c"));
List<String> view = Collections.unmodifiableList(mutable);
view.add("d"); // throws UnsupportedOperationException
mutable.add("d"); // legal — and the view sees the change
System.out.println(view); // [a, b, c, d]Use estas quando você quiser expor uma coleção interna sem copiá-la e sem dar aos chamadores permissão para mutar. O padrão clássico é um getter:
public List<String> getNames() {
return Collections.unmodifiableList(this.names);
}O chamador não pode alterar this.names através da visão retornada. Você pode. Se você também quiser proibir a si mesmo, copie:
return List.copyOf(this.names);…que é a terceira estratégia.
List.copyOf, Set.copyOf, Map.copyOf — snapshot, depois congelar
Um atalho para "copie o conteúdo atual em uma nova coleção imutável":
List<String> snapshot = List.copyOf(mutable);Após esta chamada, snapshot é completamente independente de mutable. Mudanças subsequentes em mutable são invisíveis através de snapshot. Há também uma otimização inteligente: se a fonte já é uma coleção não modificável produzida por List.of / List.copyOf, a chamada retorna a própria fonte — zero alocação.
copyOf rejeita elementos null, igual ao of. Se sua fonte pode conter null, use Collections.unmodifiableList(new ArrayList<>(source)) em vez disso.
Os três padrões em resumo
| Padrão | Independente da fonte? | Permite null? | Use quando |
|---|---|---|---|
List.of("a", "b") | n/a (sem fonte) | Não | Constantes literais |
List.copyOf(source) | Sim — armazenamento próprio | Não | Snapshot em um momento no tempo |
Collections.unmodifiableList(source) | Não — visão | Sim | Expor estado interno somente leitura |
Quando o ponto de chamada é "isto é literalmente dado codificado?" use of. Quando é "quero um snapshot congelado do que está aqui agora," use copyOf. Quando é "quero que o conteúdo atual seja observável mas não modificável através desta referência," use unmodifiableList.
Superficial, não profundo
As três estratégias são superficiais — elas congelam a estrutura da coleção, não os elementos dentro dela.
List<int[]> arrays = List.of(new int[]{1, 2}, new int[]{3, 4});
arrays.add(new int[]{5}); // UnsupportedOperationException
arrays.get(0)[0] = 99; // OK — and now the list contains {99, 2}Se você quiser imutabilidade profunda, precisa escolher tipos de elementos que sejam eles próprios imutáveis. Records com campos primitivos ou String são. Records com campos mutáveis não são. Esta é a mesma ressalva que se aplica a referências final em geral: o vínculo é fixo, o alvo pode não ser.
Set.of e Map.of têm ordem de iteração não especificada
Duas escolhas de design intencionais que pegam as pessoas de surpresa:
Set.ofeMap.ofrandomizam deliberadamente a ordem de iteração entre execuções do mesmo JVM. Se você escrever código que depende de uma ordem específica destes, verá testes instáveis. UseList.of(que preserva a ordem literal) ou umLinkedHashSet/LinkedHashMapenvolto comCollections.unmodifiable*quando você realmente precisar de ordem.Set.of(a, b)eSet.of(b, a)podem iterar diferentemente mesmo na mesma execução se os valores tiverem hashes diferentes. Não compare por toString.
Isso é intencional — Java está impedindo você de depender acidentalmente de uma ordem para que a implementação seja livre de alterá-la.
O que a não modificabilidade não oferece
- Não é thread-safe para leituras de campos de elementos mutáveis. Se os elementos são mutáveis e outra thread os está alterando, você precisa de sincronização independentemente.
- Não torna a coleção subjacente thread-safe.
Collections.unmodifiableList(arrayList)é uma visão de uma lista não thread-safe; se outra thread fizeraddemarrayList, a leitura através da visão pode ver um estado inconsistente. Para imutabilidade thread-safe,List.copyOf(ouList.of) é a ferramenta certa — elas possuem armazenamento privado. - Não torna
.equalsindependente de ordem. UmaListretornada porList.ofainda é igual-por-posição a outras listas, não por conteúdo.
Um exemplo trabalhado: literais, snapshots, visões, a armadilha da superficialidade
O programa abaixo mostra as três estratégias lado a lado, demonstra a surpresa de "a visão vê mutações", a promessa de "a cópia é independente", e a armadilha da superficialidade que pega todo mundo pela primeira vez.
O que extrair da execução:
List.ofeList.copyOfproduzem ambos uma coleção verdadeiramente imutável — elas rejeitam toda mutação. Diferem apenas em se você forneceu os dados literalmente ou os copiou de outro lugar.- A visão
Collections.unmodifiableListrejeitouview.addmas aceitoubacking.addatravés da referência original. Mudanças através da lista de apoio tornaram-se visíveis através da visão. Essa é a característica definidora de uma visão, e a razão pela qual esta estratégia não é um substituto paracopyOfem código não confiável. - A armadilha da superficialidade é real: os elementos
int[]de umaList<int[]>imutável são eles próprios mutáveis, e editar um reescreve a lista "congelada." Se você quiser imutabilidade profunda, seus elementos já devem ser imutáveis. Set.ofrejeitou a duplicata, eMap.ofrejeitou o valornull— ambos na construção. Essas coleções falham rápido e com ruído; isso é uma funcionalidade.List.copyOfde uma lista já imutável retornou a mesma instância sem alocar. Essa é a otimização do JDK, e é por isso que "sempre copie na saída" é barato quando a fonte já é imutável.
O que vem a seguir — e em direção à Parte 12
Isso encerra a parte do Collections Framework. Você agora conhece cada implementação (ArrayList, LinkedList, HashMap, TreeMap, as filas, os deques e o restante), cada interface (Collection, List, Set, Map, Queue, Deque), os cursores de iteração (Iterator, ListIterator), as interfaces de ordenação (Comparable, Comparator), a caixa de ferramentas estática (Collections) e a história da imutabilidade.
A próxima parte — Programação Funcional — muda de direção. Em vez de como armazenar dados, ela aborda como expressar transformações em dados. O primeiro capítulo, Programação Funcional em Java, introduz o modelo mental: funções como valores, imutabilidade, funções puras e composição. A partir daí, a parte constrói lambdas, referências de métodos, as interfaces funcionais integradas (Function, Predicate, Consumer, Supplier), Optional e streams — que usam as coleções que você acabou de aprender como fonte e destino.
A maioria dos padrões desta parte — list.sort(Comparator.comparing(Person::name)), map.getOrDefault(k, 0), stream().filter(...).toList() — já tem sabor funcional. A Parte 12 torna esse sabor explícito e mostra como usá-lo para todo o resto.