1. O problema da mutabilidade das coleções
Em Java, as coleções lembram um estoque com produtos: qualquer um pode chegar e adicionar, remover, alterar algo. Às vezes isso é conveniente, mas em programas grandes vira dor de cabeça. Imagine que você expôs uma lista de produtos do seu classe para fora, e alguém foi lá e removeu metade dos itens. Ou — ainda mais “divertido” — em um programa multithread uma thread adiciona elementos e outra os lê: o resultado pode ser inesperado e os erros — difíceis de detectar (por exemplo, ConcurrentModificationException).
Aqui vai um exemplo de por que a mutabilidade das coleções é uma fonte de bugs:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Chá");
products.add("Café");
}
public List<String> getProducts() {
// PERIGO! Retornando a referência para a lista interna
return products;
}
}
public class Main {
public static void main(String[] args) {
Inventory inv = new Inventory();
List<String> external = inv.getProducts();
external.remove("Chá"); // Ops! Agora não há chá no inventário
System.out.println(inv.getProducts()); // [Café]
}
}
Percebeu a armadilha? Um método retorna a coleção interna, outro a modifica. Assim, você pode destruir acidentalmente dados que deveriam estar protegidos.
2. Criando coleções imutáveis: Collections.unmodifiable*
Para evitar confusões desse tipo, o Java oferece uma proteção eficaz: você pode tornar a coleção “imutável” com wrappers especiais da classe Collections:
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
Como funciona? Primeiro você cria uma coleção normal e depois a envolve em um wrapper “imutável”:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Chá");
drinks.add("Café");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
System.out.println(immutableDrinks); // [Chá, Café]
// Vamos tentar adicionar um elemento
immutableDrinks.add("Cacau"); // Bum! UnsupportedOperationException
}
}
Uma tentativa de alterar tal coleção resulta na exceção UnsupportedOperationException. É como colar na caixa um adesivo enorme "NÃO TOCAR!" — e todo mundo que tentar adicionar ou remover algo leva um “puxão de orelha” (ou na pilha de chamadas).
Exemplo: protegendo o estado interno
Vamos corrigir nossa classe Inventory do exemplo anterior:
import java.util.*;
public class Inventory {
private List<String> products = new ArrayList<>();
public Inventory() {
products.add("Chá");
products.add("Café");
}
public List<String> getProducts() {
// Agora retornamos o wrapper
return Collections.unmodifiableList(products);
}
}
Agora, se alguém tentar modificar a lista obtida, receberá uma exceção.
3. Comportamento de coleções imutáveis: proteção superficial
É importante entender: unmodifiableList e seus “irmãos” fazem apenas um invólucro em torno da coleção de origem. Eles não criam uma cópia — quaisquer alterações na coleção de origem (a que está “por dentro”) serão visíveis também no wrapper!
Demonstração
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> drinks = new ArrayList<>();
drinks.add("Chá");
List<String> immutableDrinks = Collections.unmodifiableList(drinks);
drinks.add("Café"); // Alteramos a coleção de origem
System.out.println(immutableDrinks); // [Chá, Café] — o elemento apareceu!
}
}
Conclusão: o wrapper protege apenas contra alterações via o próprio wrapper. Se alguém mantiver uma referência à coleção de origem, ainda poderá modificá-la.
4. Imutabilidade profunda: mitos e realidade
Os wrappers unmodifiable* tornam a coleção imutável apenas por fora. Mas se a coleção contiver objetos mutáveis, eles podem ser alterados!
Exemplo
import java.util.*;
class Product {
String name;
Product(String name) {
this.name = name;
}
public String toString() {
return name;
}
}
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Chá"));
List<Product> immutableProducts = Collections.unmodifiableList(products);
// Alteramos o objeto dentro da coleção
immutableProducts.get(0).name = "Café";
System.out.println(immutableProducts); // [Café]
}
}
Conclusão:
- A coleção é “imutável”, mas os objetos dentro dela não são.
- Para imutabilidade completa (profunda), use objetos imutáveis (por exemplo, String, Integer, classes record ou torne suas próprias classes imutáveis).
5. Quando usar coleções imutáveis
Para proteger o estado interno
Se você escreve uma classe que armazena uma coleção e a expõe, sempre retorne um wrapper para que ninguém possa alterar seus dados acidentalmente (ou de propósito):
public List<String> getProducts() {
return Collections.unmodifiableList(products);
}
Em programas multithread
Em aplicativos multithread, coleções mutáveis são uma fonte de problemas (race conditions, ConcurrentModificationException e outras “alegrias da vida”). Se a coleção não precisa ser alterada após a criação, torne-a imutável.
Para transferência de dados entre camadas
Se você passa uma coleção de uma camada do programa para outra (por exemplo, do DAO para o serviço), passe uma cópia imutável ou um wrapper — isso protege contra alterações acidentais.
6. Exemplos práticos
Exemplo 1: protegendo a lista de alunos
import java.util.*;
public class Group {
private final List<String> students = new ArrayList<>();
public void addStudent(String name) {
students.add(name);
}
public List<String> getStudents() {
return Collections.unmodifiableList(students);
}
}
Agora ninguém poderá adicionar ou remover um aluno diretamente via getStudents().
Exemplo 2: Map imutável
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> grades = new HashMap<>();
grades.put("Vasya", 5);
grades.put("Masha", 4);
Map<String, Integer> immutableGrades = Collections.unmodifiableMap(grades);
// immutableGrades.put("Petya", 3); // UnsupportedOperationException
}
}
7. Nuances úteis
Alternativas modernas: List.of, Set.of, Map.of
No Java 9 surgiram maneiras ainda mais convenientes de criar coleções imutáveis:
List<String> drinks = List.of("Chá", "Café");
Set<String> fruits = Set.of("Maçã", "Banana");
Map<String, Integer> ages = Map.of("Vasya", 20, "Masha", 21);
- Essas coleções são imutáveis (qualquer tentativa de alteração resulta em exceção).
- Elas não têm uma coleção “de origem” mutável (ao contrário de Collections.unmodifiable*).
- Não permitem valores null.
Além disso, a partir do Java 10 há métodos de cópia: List.copyOf, Set.copyOf, Map.copyOf — criam uma cópia imutável da coleção fornecida.
Comparação de maneiras de criar coleções imutáveis
| Método | Imutabilidade profunda | É possível alterar a coleção de origem? | Permite null? | Versão do Java |
|---|---|---|---|---|
|
Não | Sim | Sim | 1.2 |
|
Não | Não (não há coleção de origem) | Não | 9+ |
8. Erros comuns ao trabalhar com coleções imutáveis
Erro nº 1: Alterar a coleção de origem após criar o wrapper. Você criou unmodifiableList, e depois alguém altera a lista de origem. O wrapper não protege disso — as mudanças serão visíveis em todos os lugares onde o wrapper for usado.
Erro nº 2: Esperar imutabilidade profunda. Muitos pensam que, se a coleção é imutável, então os objetos dentro dela também não podem ser alterados. Na prática, apenas a estrutura é protegida (adição/remoção/alteração via a coleção), não o conteúdo dos objetos.
Erro nº 3: Usar coleções imutáveis com valor null nas fábricas modernas. Coleções criadas por List.of, Set.of, Map.of não permitem null. Tentar adicionar ou obter null lançará uma exceção.
Erro nº 4: Expor referências a coleções mutáveis. Se você retorna para fora uma referência à coleção interna (sem wrapper), você perde o controle sobre seus dados — caminho direto para bugs e “vazamento” de invariantes.
Erro nº 5: Usar coleções imutáveis em código que espera mutabilidade. Se código de terceiros tentar modificar a coleção (por exemplo, adicionar um elemento), ele receberá UnsupportedOperationException. Garanta que os consumidores saibam da imutabilidade dos dados.
GO TO FULL VERSION