W3docs

Classes Imutáveis em Java

Projete classes imutáveis em Java com campos final, cópias defensivas e sem setters.

Uma classe imutável é aquela cujas instâncias não podem ser alteradas após a construção. String, Integer, LocalDate, BigDecimal, UUID — a biblioteca padrão do Java está cheia delas, e não por acidente. Objetos imutáveis são seguros para compartilhar entre threads, seguros para usar como chaves de HashMap, seguros para armazenar em cache e fáceis de raciocinar: uma vez que você os viu, você sabe o estado deles pelo restante de sua vida.

Tornar uma classe imutável não se trata de adicionar uma única palavra-chave — trata-se de seguir um conjunto de regras em conjunto. Perca uma e você terá uma classe que parece imutável, mas não é.

As cinco regras

Para tornar uma classe genuinamente imutável:

  1. Declare a classe como final (ou use apenas construtores privados). Caso contrário, uma subclasse pode quebrar o contrato.
  2. Torne cada campo private final. final impede a reatribuição após a construção; private impede que os chamadores os toquem diretamente.
  3. Não exponha setters. Qualquer método de mutação (add, set, clear, reset) está fora.
  4. Copie defensivamente entradas mutáveis no construtor. Se o chamador passar um Date ou uma List, copie-o — caso contrário, ele pode mutá-lo de fora e seu objeto "imutável" muda por baixo de você.
  5. Copie defensivamente retornos mutáveis nos getters — pela mesma razão, ao contrário.

Uma classe que faz as cinco é profundamente imutável. Perca qualquer uma delas e a garantia vaza.

Aviso

final sozinho não é imutabilidade. Um campo final não pode ser reatribuído, mas se ele apontar para um objeto mutável — uma List, um array, um Date — esse objeto ainda pode mudar. final List<String> tags significa que você não pode trocar a lista por uma diferente, não que o conteúdo da lista esteja congelado. As regras 4 e 5 existem precisamente para fechar essa lacuna. Veja Java final keyword para o que final promete e não promete.

O exemplo mínimo

Para uma classe cujos campos são todos primitivos ou já imutáveis, as regras se reduzem a quase nada:

public final class Point {
  private final int x;
  private final int y;

  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  public int x() { return x; }
  public int y() { return y; }
}

int é um primitivo, portanto não há nada a copiar defensivamente. A classe é final, os campos são private final, não existem setters. Pronto.

Campos mutáveis precisam de cópias defensivas

O problema começa quando um campo é em si mutável — um array, um Date, um ArrayList. Se você armazenar a referência do chamador diretamente, ele mantém um identificador para ela e pode mutar seus internos:

// Broken: the array is shared
public final class Trajectory {
  private final double[] points;
  public Trajectory(double[] points) { this.points = points; }
  public double[] points() { return points; }
}

double[] arr = {1.0, 2.0, 3.0};
Trajectory t = new Trajectory(arr);
arr[0] = 999;                     // mutates the "immutable" object!
System.out.println(t.points()[0]);  // 999

A correção é copiar na entrada e na saída:

public final class Trajectory {
  private final double[] points;
  public Trajectory(double[] points) {
    this.points = points.clone();    // copy in
  }
  public double[] points() {
    return points.clone();           // copy out
  }
}

Para coleções, o equivalente é List.copyOf(other) (retorna uma lista não modificável suportada por uma cópia):

public final class Recipe {
  private final String        name;
  private final List<String>  steps;
  public Recipe(String name, List<String> steps) {
    this.name  = name;
    this.steps = List.copyOf(steps);   // copy + unmodifiable view
  }
  public List<String> steps() { return steps; }   // already unmodifiable
}

Observe a assimetria com o exemplo do array: o clone() de um array produz uma cópia mutável, então você deve copiar novamente na saída. List.copyOf produz uma lista não modificável, então o getter pode entregá-la diretamente — qualquer chamador que tente mutá-la recebe uma UnsupportedOperationException. Prefira tipos de coleção imutáveis quando puder; eles eliminam toda uma classe de erros de cópia na saída.

"Modificações" retornam novas instâncias

Uma classe imutável ainda pode suportar mudanças — retornando uma nova instância:

public final class Money {
  private final long cents;
  public Money plus(Money other)  { return new Money(cents + other.cents); }
  public Money times(int factor)  { return new Money(cents * factor); }
  // constructor + accessors omitted
}

A convenção nomeia o método with... quando ele produz uma cópia com um campo alterado: point.withX(5), user.withEmail("..."). A API de data/hora do Java usa esse padrão de forma consistente — LocalDate.plusDays(7), LocalDate.withYear(2026).

Por que isso importa

Objetos imutáveis oferecem:

  • Thread safety de graça. Sem bloqueios, sem volatile, sem surpresas de visibilidade — não há nada para sincronizar porque o estado não pode mudar.
  • Compartilhamento e cache seguros. Dois chamadores que têm o mesmo Money(2000, "USD") não podem interferir um com o outro.
  • Chaves de hash confiáveis. Como os campos usados em hashCode não podem mudar, o bucket do objeto nunca fica desatualizado. Uma chave mutável cujo hash muda depois que ela é armazenada em um HashMap fica efetivamente perdida — veja Java equals and hashCode.
  • Raciocínio mais fácil. Uma vez que você viu um objeto imutável, você sabe o que ele fará pelo restante de sua vida. Sem arqueologia de "onde isso foi mutado?".

O custo é alocar novas instâncias para cada "modificação". Para objetos pequenos e frequentemente usados (String, Integer), isso raramente é um problema; a JVM é muito boa em alocações de curta duração. Para casos genuinamente caros, existem técnicas específicas (string builders, estruturas de dados persistentes) — mas recorra a elas apenas quando o profiling mostrar um problema real.

Records fazem a maior parte do trabalho

Um record é implicitamente final, tem campos private final, gera acessadores sem setters e oferece equals/hashCode/toString de graça:

public record Point(int x, int y) {}

Isso é profundamente imutável, desde que os próprios componentes sejam imutáveis. Para records que contêm um componente mutável (uma List, um array), você ainda precisa de um construtor compacto que copie defensivamente:

public record Recipe(String name, List<String> steps) {
  public Recipe {
    steps = List.copyOf(steps);
  }
}

Quando records se encaixam, eles são o caminho mais curto para uma classe imutável correta.

Um exemplo prático

java— editable, runs on the server

O que vem a seguir

Classes imutáveis são sobre controlar mudanças. O capítulo final da Parte 6 é sobre controlar quantidade — uma classe projetada para que apenas uma instância exista. Continue em Java singleton pattern.

Prática

Prática
Por que é necessária uma cópia defensiva no construtor de uma classe imutável que contém uma entrada mutável como uma `List` ou um array?
Por que é necessária uma cópia defensiva no construtor de uma classe imutável que contém uma entrada mutável como uma `List` ou um array?
Was this page helpful?