CodeGym /Cursos /JAVA 25 SELF /Coleções imutáveis: Collections.unmodifiable

Coleções imutáveis: Collections.unmodifiable

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

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
Collections.unmodifiableList(list)
Não Sim Sim 1.2
List.of(...), Set.of(...), Map.of(...)
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.

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