Java Records
Muitas classes existem só para carregar dados: um aluno com nome e nota, um produto com nome e preço, um ponto com x e y. Para fazer uma classe dessas "do jeito certo", você escreve construtor, getters, toString, equals e hashCode: umas quarenta linhas para dizer "um aluno tem nome e nota". O record, do Java 16, diz exatamente isso em uma linha, e o compilador escreve o resto.
- Como declarar um
recorde o que ele gera sozinho (com uma espiada pelojavap). - Os acessores
nome()(semget), otoStringe oequalsprontos. - Por que um record é imutável, e como "alterar" um criando outro.
- O construtor compacto, para validar e ajustar os dados.
- O que um record não pode ter, e quando usar record ou classe comum.
Java 16+ Records existem desde o Java 16. Num JDK 11 ou 8, use uma classe comum (a seção "O que o record gera" mostra como ela ficaria).
A ideia: um formulário preenchido
Pense numa ficha de inscrição já preenchida: nome, data, nota. Ela não "faz" nada; ela registra informações. Duas fichas com os mesmos dados são, para todos os efeitos, iguais. E, uma vez entregue, ninguém rasura: se algo mudou, preenche-se uma ficha nova.
Um record ("registro", em inglês) é isso em Java: um objeto que é só a soma dos seus dados, que se compara pelos dados e que não muda depois de criado.
Um record em uma linha
public class Main {
public static void main(String[] args) {
Aluno ana = new Aluno("Ana", 9.5);
Aluno outra = new Aluno("Ana", 9.5);
Aluno bruno = new Aluno("Bruno", 6.0);
System.out.println(ana);
System.out.println(ana.nome() + " tirou " + ana.nota());
System.out.println(ana.equals(outra));
System.out.println(ana.equals(bruno));
System.out.println(ana == outra);
System.out.println(ana.hashCode() == outra.hashCode());
}
}
record Aluno(String nome, double nota) {
}
- record
- No lugar de
class: "este tipo é um registro de dados". - Aluno
- O nome do tipo.
- (String nome, double nota)
- Os componentes do record: os dados que ele carrega, com tipo e nome, como parâmetros de um método. Daqui o compilador tira tudo o mais.
- { }
- O corpo. Pode ficar vazio, ou receber métodos (você verá adiante).
$ java Main.java Aluno[nome=Ana, nota=9.5] « toString pronto: Tipo[componente=valor, …] Ana tirou 9.5 « acessores nome() e nota() true « equals: mesmos dados = iguais false false « == : continuam sendo dois objetos diferentes true « hashCode igual para dados iguais
Repare na saída: o println mostrou os dados, sem você escrever toString (compare com o Aluno@365c30cc de Sobrescrita). O equals diz que duas "Anas com 9,5" são iguais, comparando os dados. O == continua dizendo que são dois objetos distintos na memória (a diferença entre os dois é o assunto de A Classe Object).
Missão: crie o record Produto, com os componentes String nome e double preco. No main, crie o produto "Café" de 18.90 e mostre-o com println. A saída deve ser Produto[nome=Café, preco=18.9]. Depois, mostre só o preço com o acessor.
public class Main {
public static void main(String[] args) {
// crie e mostre o produto
}
}
// crie aqui o record Produto
public class Main {
public static void main(String[] args) {
Produto cafe = new Produto("Café", 18.90);
System.out.println(cafe);
System.out.println(cafe.preco());
}
}
record Produto(String nome, double preco) {
}
O que o record gera
Para ver o que o compilador escreveu, a Ana compilou o record sozinho num arquivo e usou o javap, que lista os membros de uma classe compilada (você o conheceu em A Biblioteca Padrão):
public record Aluno(String nome, double nota) {
}
$ javac Aluno.java
$ javap Aluno.class
Compiled from "Aluno.java"
public final class Aluno extends java.lang.Record {
public Aluno(java.lang.String, double);
public final java.lang.String toString();
public final int hashCode();
public final boolean equals(java.lang.Object);
public java.lang.String nome();
public double nota();
}
Linha por linha, o que uma linha de record virou:
| Gerado | O que é |
|---|---|
public final class Aluno | Uma classe final: ninguém herda de um record |
extends java.lang.Record | Todo record herda de Record (e por isso não pode herdar de mais nada) |
Aluno(String, double) | O construtor canônico: recebe todos os componentes, na ordem |
toString() | Aluno[nome=Ana, nota=9.5] |
hashCode() e equals(Object) | Baseados em todos os componentes |
nome(), nota() | Os acessores: um por componente, com o mesmo nome (sem get) |
(não aparece no javap) | Os atributos private final String nome e private final double nota |
Os atributos não aparecem porque são private, e o javap sem opções mostra só o que é visível de fora. Para comparar, a classe comum equivalente, que você escreveria antes do Java 16, seria assim:
import java.util.Objects;
public final class Aluno {
private final String nome;
private final double nota;
public Aluno(String nome, double nota) {
this.nome = nome;
this.nota = nota;
}
public String nome() { return nome; }
public double nota() { return nota; }
@Override
public boolean equals(Object o) {
if (!(o instanceof Aluno outro)) return false;
return Objects.equals(nome, outro.nome) && Double.compare(nota, outro.nota) == 0;
}
@Override
public int hashCode() {
return Objects.hash(nome, nota);
}
@Override
public String toString() {
return "Aluno[nome=" + nome + ", nota=" + nota + "]";
}
}
Trinta linhas, com vários lugares para errar (esquecer um campo no equals, por exemplo). O record faz o mesmo em uma.
nome() e não getNome()?
A convenção getNome() vem das classes comuns (veja Encapsulamento). Os criadores dos records preferiram que o acessor tenha exatamente o nome do componente. Na prática: em records, ana.nome(); em classes comuns, ana.getNome(). Cuidado ao misturar.
Imutável: sem setters
Os atributos de um record são private final. Não há como mudá-los depois da criação: nem diretamente, nem por um setter, que simplesmente não existe:
public class Main {
public static void main(String[] args) {
Aluno ana = new Aluno("Ana", 9.5);
ana.nome = "Ana Souza";
ana.setNome("Ana Souza");
}
}
record Aluno(String nome, double nota) {
}
$ javac Main.java
Main.java:4: error: nome has private access in Aluno
ana.nome = "Ana Souza";
^
Main.java:5: error: cannot find symbol
ana.setNome("Ana Souza");
^
symbol: method setNome(String)
location: variable ana of type Aluno
2 errors
Para "mudar" um record, crie outro com os dados novos. É o que a ficha preenchida faria. Um padrão comum é um método que devolve a cópia alterada:
public class Main {
public static void main(String[] args) {
Produto cafe = new Produto("Café", 18.0);
Produto promocao = cafe.comDesconto(10);
System.out.println(cafe);
System.out.println(promocao);
}
}
record Produto(String nome, double preco) {
Produto comDesconto(double percentual) {
return new Produto(nome, preco - preco * percentual / 100);
}
}
$ java Main.java Produto[nome=Café, preco=18.0] « o original não mudou Produto[nome=Café, preco=16.2] « um produto novo, com 10% a menos
Por que imutável é bom? Um objeto que não muda pode ser compartilhado à vontade, guardado em listas, usado como chave de um HashMap ou passado para outros métodos sem medo de alguém alterá-lo pelas suas costas. Mas lembre-se do que você viu em final: se um componente for uma lista mutável (List<String> materias com um ArrayList), o record não troca a lista, mas a lista em si ainda aceita add.
O construtor compacto: validar os dados
Uma nota 11 não deveria existir. Um record pode conferir os dados na criação com um construtor compacto: um construtor sem a lista de parâmetros, que roda antes de os valores serem guardados nos atributos.
public class Main {
public static void main(String[] args) {
Aluno ana = new Aluno(" Ana ", 9.5);
System.out.println(ana + " aprovado? " + ana.aprovado());
Aluno erro = new Aluno("Bruno", 11);
System.out.println(erro);
}
}
record Aluno(String nome, double nota) {
Aluno {
if (nota < 0 || nota > 10) {
throw new IllegalArgumentException("nota inválida: " + nota);
}
nome = nome.trim();
}
boolean aprovado() {
return nota >= 6;
}
}
- Aluno {
- O construtor compacto: o nome do record e as chaves, sem parênteses. Os parâmetros (
nome,nota) existem automaticamente. - throw new IllegalArgumentException(…)
- Se a nota é inválida, recusa a criação lançando uma exceção (veja throw e throws). O objeto não chega a existir.
- nome = nome.trim();
- Ajusta o parâmetro antes de ele ser guardado: tira os espaços das pontas. Repare: sem
this.. - boolean aprovado()
- Um método comum. Records podem ter quantos métodos quiserem.
$ java Main.java Aluno[nome=Ana, nota=9.5] aprovado? true « os espaços de " Ana " sumiram Exception in thread "main" java.lang.IllegalArgumentException: nota inválida: 11.0 at Aluno.<init>(Main.java:13) at Main.main(Main.java:5)
A primeira aluna foi criada com o nome limpo. A segunda foi recusada. No rastro, Aluno.<init> é como a JVM chama um construtor (init de initialize, "inicializar"), e Main.java:13 é a linha do throw. A nota aparece como 11.0 porque o 11 virou double.
O que acontece por baixo: o compilador pega o seu construtor compacto e acrescenta, no fim, this.nome = nome; this.nota = nota;. Por isso você mexe nos parâmetros, e não nos atributos.
O que um record pode e não pode ter
| Pode | Não pode |
|---|---|
Métodos de instância e static | Atributos de instância além dos componentes |
Atributos static (constantes) | extends (já herda de Record) |
Implementar interfaces (implements Comparable<Aluno>) | Ser herdado (é final) |
Construtor compacto e outros construtores (que chamam o canônico com this(…)) | Setters que mudem os componentes |
Sobrescrever toString, um acessor etc. | Componentes que mudam de valor |
Tentando as duas proibições mais comuns:
$ javac Main.java
Main.java:9: error: '{' expected
record Aluno(String nome, double nota) extends Pessoa {
^
Main.java:13: error: field declaration must be static
int idade;
^
(consider replacing field with record component)
2 errors
- '{' expected: depois dos componentes, o compilador esperava o corpo;
extendsnão faz parte da gramática do record. Se precisa de um "tipo pai", use uma interface. - field declaration must be static = "a declaração de atributo precisa ser
static", e a dica: consider replacing field with record component, "considere trocar o atributo por um componente do record": acrescenteint idadeentre os parênteses.
Record ou classe comum?
| Situação | Use |
|---|---|
| O objeto é só um conjunto de dados (DTO, resultado de uma consulta, coordenada, item de pedido) | record |
| Dois objetos com os mesmos dados devem ser considerados iguais | record |
| Devolver dois ou mais valores de um método | record (até local, dentro do método) |
| O objeto muda com o tempo (uma conta com saldo, um carrinho de compras) | Classe comum |
| Precisa herdar de outra classe ou ser herdado | Classe comum |
| Precisa esconder os dados (não expor um acessor para cada um) | Classe comum |
| Identidade importa mais que os dados (duas pessoas homônimas são pessoas diferentes) | Classe comum |
Records também combinam com recursos modernos: os record patterns do Java 21 desmontam um record direto no instanceof ou no switch (if (obj instanceof Aluno(String n, double nota))); veja Pattern Matching.
Quando dá errado
O que aconteceu: o acessor de um record tem o nome do componente, sem get.
Como resolver: ana.nome().
$ javac Main.java
Main.java:4: error: nome has private access in Aluno
ana.nome = "Ana Souza";
^
Main.java:5: error: cannot find symbol
ana.setNome("Ana Souza");
^
symbol: method setNome(String)
location: variable ana of type Aluno
2 errors
O que aconteceu: tentativa de alterar um record (que é imutável).
Como resolver: crie um record novo: ana = new Aluno("Ana Souza", ana.nota());, ou escreva um método que devolva a cópia alterada.
O que aconteceu: um atributo comum declarado no corpo do record.
Como resolver: transforme-o em componente (entre os parênteses). Se for uma constante, static final.
<init> no rastro
O que aconteceu: o construtor compacto recusou os dados (foi você que programou isso!).
Como resolver: confira os valores passados no new apontado pela linha seguinte do rastro (at Main.main(Main.java:5)).
Referência
| Escrita | O que faz |
|---|---|
record Aluno(String nome, double nota) { } | Declara o record com dois componentes |
new Aluno("Ana", 9.5) | Construtor canônico (todos os componentes, na ordem) |
ana.nome() | Acessor (sem get) |
System.out.println(ana) | Aluno[nome=Ana, nota=9.5] |
a.equals(b) | true se todos os componentes são iguais |
Aluno { … } | Construtor compacto: validar e ajustar os parâmetros |
record … implements Comparable<Aluno> | Records podem implementar interfaces |
Exercícios
1. Declarar
Complete a declaração de um ponto com x e y inteiros:
2. O acessor
Com Ponto p = new Ponto(3, 4);, complete para pegar o x:
3. O toString pronto
O que System.out.println(new Ponto(3, 4)) mostra?
4. new Ponto(1, 2).equals(new Ponto(1, 2)) vale…
5. Qual destas coisas um record não pode ter?
6. Qual destes é o melhor candidato a record?
Missão final: o record Produto é imutável, então o desconto precisa criar um produto novo. Escreva no record o método Produto comDesconto(double percentual), que devolve new Produto(nome, preco - preco * percentual / 100). A saída deve mostrar o original intacto (preco=18.0) e a promoção (preco=16.2).
public class Main {
public static void main(String[] args) {
Produto cafe = new Produto("Café", 18.0);
Produto promocao = cafe.comDesconto(10);
System.out.println(cafe);
System.out.println(promocao);
}
}
record Produto(String nome, double preco) {
// escreva aqui o método comDesconto
}
public class Main {
public static void main(String[] args) {
Produto cafe = new Produto("Café", 18.0);
Produto promocao = cafe.comDesconto(10);
System.out.println(cafe);
System.out.println(promocao);
}
}
record Produto(String nome, double preco) {
Produto comDesconto(double percentual) {
return new Produto(nome, preco - preco * percentual / 100);
}
}
Resumo
record Aluno(String nome, double nota) { }cria uma classefinalcom atributosprivate final, construtor, acessoresnome()/nota(),equals,hashCodeetoString.- Records se comparam pelos dados: dois com os mesmos componentes são
equals. - São imutáveis: para "mudar", crie outro (
comDescontodevolve um produto novo). - O construtor compacto (
Aluno { … }) valida e ajusta os parâmetros antes de guardá-los. - Não podem ter
extendsnem atributos de instância extras; podem ter métodos e implementar interfaces. - Use record para dados; classe comum para objetos que mudam, que herdam ou que escondem os dados.