W3docs

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:

  1. 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.
  2. Hashável com segurança. Colocar uma List mutável em um HashSet é um bug — se o conteúdo da lista mudar, seu hashCode muda, e o set perde o elemento. Coleções não modificáveis evitam isso completamente.
  3. 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:

  1. Sem elementos null, sem chaves null, sem valores null. List.of("a", null) lança NullPointerException na construção. Se você precisar representar "ausente," use Optional ou omita a chave do mapa.
  2. Sem duplicatas para Set.of e Map.of. Set.of("a", "a") lança IllegalArgumentException. Elas são destinadas a dados literais que você controla.
  3. Map.of tem sobrecargas apenas até 10 entradas. Para 11 ou mais, use Map.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ãoIndependente da fonte?Permite null?Use quando
List.of("a", "b")n/a (sem fonte)NãoConstantes literais
List.copyOf(source)Sim — armazenamento próprioNãoSnapshot em um momento no tempo
Collections.unmodifiableList(source)Não — visãoSimExpor 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:

  1. Set.of e Map.of randomizam 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. Use List.of (que preserva a ordem literal) ou um LinkedHashSet/LinkedHashMap envolto com Collections.unmodifiable* quando você realmente precisar de ordem.
  2. Set.of(a, b) e Set.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 fizer add em arrayList, a leitura através da visão pode ver um estado inconsistente. Para imutabilidade thread-safe, List.copyOf (ou List.of) é a ferramenta certa — elas possuem armazenamento privado.
  • Não torna .equals independente de ordem. Uma List retornada por List.of ainda é 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.

java— editable, runs on the server

O que extrair da execução:

  • List.of e List.copyOf produzem 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.unmodifiableList rejeitou view.add mas aceitou backing.add atravé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 para copyOf em código não confiável.
  • A armadilha da superficialidade é real: os elementos int[] de uma List<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.of rejeitou a duplicata, e Map.of rejeitou o valor null — ambos na construção. Essas coleções falham rápido e com ruído; isso é uma funcionalidade.
  • List.copyOf de uma lista 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.

Prática

Prática
Você tem um campo `private List<String> names = new ArrayList<>()` e quer um getter que permita aos chamadores *ler* o conteúdo atual mas nunca mutá-lo, e que também reflita adições posteriores a `names` feitas pela própria classe. Qual expressão de retorno se encaixa?
Você tem um campo `private List<String> names = new ArrayList<>()` e quer um getter que permita aos chamadores *ler* o conteúdo atual mas nunca mutá-lo, e que também reflita adições posteriores a `names` feitas pela própria classe. Qual expressão de retorno se encaixa?
Was this page helpful?