W3docs

Java Records em Profundidade

Uma análise aprofundada dos records Java — construtores canônicos e compactos, validação e casos de uso.

Um record é a forma do Java de declarar uma classe cuja única função é carregar dados. Introduzido como prévia no Java 14 e finalizado no Java 16, um record elimina o boilerplate usual — campos private final, um construtor, acessores, equals, hashCode e toString — em uma única linha de cabeçalho. O capítulo anterior sobre records mostrou a sintaxe básica; este vai mais fundo em como os records realmente se comportam: seus construtores canônicos e compactos, como eles impõem invariantes, quais garantias de imutabilidade você obtém e onde eles se encaixam (e onde não se encaixam).

O que o compilador gera para você

Quando você escreve record Point(int x, int y) {}, o compilador emite uma classe final com dois campos private final, um construtor público que recebe ambos, métodos acessores públicos nomeados exatamente como os componentes (x(), y() — sem prefixo get), e equals, hashCode e toString baseados em valor.

record Point(int x, int y) {}

// Equivalent to (roughly) hand-writing:
// final class Point {
//   private final int x;
//   private final int y;
//   Point(int x, int y) { this.x = x; this.y = y; }
//   int x() { return x; }
//   int y() { return y; }
//   public boolean equals(Object o) { ... compares x and y ... }
//   public int hashCode() { ... derived from x and y ... }
//   public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
// }

O x e o y no cabeçalho são os componentes do record. Os membros gerados pelo compilador são derivados inteiramente deles, na ordem de declaração.

Construtores canônicos e compactos

Todo record tem um construtor canônico cujos parâmetros correspondem aos componentes. Você raramente o escreve por completo — em vez disso, usa o construtor compacto, que omite a lista de parâmetros e as atribuições finais this.field = field. O compilador executa seu código primeiro e depois atribui os parâmetros (possivelmente modificados) aos campos. É o lugar natural para validação e normalização.

record Range(int low, int high) {
  Range {                                  // compact constructor — no (int low, int high)
    if (low > high) {
      throw new IllegalArgumentException("low must be <= high");
    }
    low = Math.max(low, 0);                // reassigning the parameter normalizes the field
  }
}

Se você precisar da forma canônica explícita (por exemplo, para copiar defensivamente um componente mutável), escreva a assinatura completa e faça as atribuições você mesmo:

record Tags(String name, List<String> values) {
  Tags(String name, List<String> values) {           // explicit canonical constructor
    this.name = name;
    this.values = List.copyOf(values);               // defensive, unmodifiable copy
  }
}

Imutabilidade e o que records não são

Os campos de um record são final, portanto a referência que cada componente mantém nunca muda após a construção. Isso torna os records superficialmente imutáveis. Mas a imutabilidade para na referência: se um componente aponta para um objeto mutável (como um ArrayList), quem compartilha esse objeto ainda pode mutar seu conteúdo. Cópias defensivas no construtor canônico fecham essa brecha.

PropriedadeRecordsClasses comuns
Campossempre private finalsua escolha
Classeimplicitamente finalextensível a menos que seja final
Superclassesempre java.lang.Recordqualquer uma (padrão Object)
Acessoresgerados automaticamente, sem prefixo getescritos à mão
equals/hashCodebaseados em valor, geradospor identidade por padrão
Settersnenhum — imutávelpermitidos

Como um record sempre estende java.lang.Record, ele não pode estender outra classe. Ele ainda pode implementar interfaces, declarar membros estáticos e adicionar métodos de instância.

Adicionando comportamento, membros estáticos e fábricas

Um record ainda é uma classe. Você pode adicionar métodos extras, métodos de fábrica estáticos, campos estáticos e até tipos aninhados. Os componentes definem o estado; todo o resto é Java comum.

record Money(String currency, long cents) {
  static Money of(String currency, long cents) {     // static factory
    return new Money(currency, cents);
  }
  Money plus(Money other) {                          // derived behavior
    if (!currency.equals(other.currency)) {
      throw new IllegalArgumentException("currency mismatch");
    }
    return new Money(currency, cents + other.cents); // returns a new value
  }
}

Records também se combinam naturalmente com tipos sealed e pattern matching, modelando conjuntos fechados de formas de dados — a base do design de dados no estilo algébrico no Java moderno. Uma interface sealed fixa o conjunto de implementações de record permitidas, e um switch sobre esses records pode desconstruir cada um por seus componentes em uma única expressão.

Um exemplo completo: records de ponta a ponta

Este programa exercita os membros gerados de um record, comprova as propriedades de imutabilidade e de classe via reflexão, impõe um invariante em um construtor compacto, lista os componentes do record na ordem de declaração e mostra records funcionando com coleções e comportamento adicionado.

java— editable, runs on the server

O que extrair da execução:

  • O Point para o qual você nunca escreveu um corpo ainda imprimiu Point[x=3, y=4], respondeu ao a.x() e reportou equals by value: true com hash codes iguais — o compilador gerou toString, acessores, equals e hashCode baseados em valor apenas a partir dos dois componentes.
  • A reflexão confirmou o contrato que a linguagem garante: is final class : true (records não podem ser subclassificados) e is a record : true (todo record estende java.lang.Record), razão pela qual não existem setters e os campos são imutáveis.
  • A chamada Range(9, 2) foi rejeitada com low must be <= high. O construtor compacto executou antes que os campos fossem atribuídos, portanto um record nunca é construído em um estado inválido — a validação pertence ali, não em uma verificação de fábrica separada.
  • getRecordComponents() retornou os componentes na ordem de declaração como low:int high:int, mostrando que a estrutura de um record é introspectável por reflexão — a base para bibliotecas de serialização e frameworks que mapeiam records automaticamente.
  • Money.of("USD", 500).plus(Money.of("USD", 250)) produziu USD 750, e distinct() colapsou dois valores idênticos Point(0,0) deixando 2 — records se comportam como valores adequados em qualquer lugar, incluindo streams e sets, precisamente porque seu equals/hashCode compara conteúdos.

Quando usar um record (e quando não usar)

Recorra a um record quando o tipo é definido pelos seus dados e esses dados não mudam após a construção:

  • DTOs e payloads de requisição/resposta de API.
  • Chaves de mapas e elementos de conjuntos (equals/hashCode baseados em valor vêm gratuitamente).
  • Tipos de retorno que agrupam vários valores, substituindo tuplas descartáveis ou parâmetros de saída.
  • As "folhas" de uma hierarquia sealed que você desconstrói com pattern matching.

Prefira uma classe comum quando:

  • O objeto tem estado mutável ou um ciclo de vida (entidades, builders, serviços).
  • Você precisa estender outra classe — records só podem implementar interfaces.
  • A identidade do objeto importa mais do que seu conteúdo (você quer igualdade por referência).

Uma armadilha comum: o acessor de um record retorna a referência armazenada como está. Se um componente é de um tipo mutável (uma List, um array, um Date), copie-o defensivamente no construtor canônico — como o exemplo Tags acima faz com List.copyOf — caso contrário, quem chamar pode mutar o estado "imutável" do record através da referência que passou.

Prática

Prática
O que o construtor compacto de um record (por exemplo 'Range { ... }') permite fazer, algo que um corpo de construtor explícito comum exigiria mais código?
O que o construtor compacto de um record (por exemplo 'Range { ... }') permite fazer, algo que um corpo de construtor explícito comum exigiria mais código?
Was this page helpful?