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.

Você vai aprender
  • Como declarar um record e o que ele gera sozinho (com uma espiada pelo javap).
  • Os acessores nome() (sem get), o toString e o equals prontos.
  • 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:

GeradoO que é
public final class AlunoUma classe final: ninguém herda de um record
extends java.lang.RecordTodo 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.

Por que 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:

Este código tem um erro de propósito
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

PodeNão pode
Métodos de instância e staticAtributos 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; extends nã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": acrescente int idade entre os parênteses.

Record ou classe comum?

SituaçãoUse
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 iguaisrecord
Devolver dois ou mais valores de um métodorecord (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 herdadoClasse 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

"cannot find symbol … method getNome()"

O que aconteceu: o acessor de um record tem o nome do componente, sem get.

Como resolver: ana.nome().

"… has private access in …" ao mudar um componente
$ 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.

"field declaration must be static"

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.

Exceção com <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

EscritaO 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:

int a = ;

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 classe final com atributos private final, construtor, acessores nome()/nota(), equals, hashCode e toString.
  • Records se comparam pelos dados: dois com os mesmos componentes são equals.
  • São imutáveis: para "mudar", crie outro (comDesconto devolve um produto novo).
  • O construtor compacto (Aluno { … }) valida e ajusta os parâmetros antes de guardá-los.
  • Não podem ter extends nem 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.