Duas formas de declarar tipos que o Java trata de maneira especial: records, para valores imutáveis, e enums, para conjuntos fixos de constantes com comportamento.
"Iguais ou o Mesmo?" terminou com a classe Ponto completa: campos privados e finais, construtor, getters, equals, hashCode e toString. Umas quarenta linhas para dizer "um ponto é um par de coordenadas imutável". Esse tipo de classe é tão comum que o Java ganhou uma forma própria de declará-la, o record. Nesta aula você também vai conhecer os enums, que representam conjuntos fixos de valores, como os naipes de um baralho ou os estados de um pedido. Em Java, eles são muito mais poderosos do que em C ou C#, porque cada constante é um objeto completo, com campos e métodos.
Records: a classe de valor em uma linha
Este é o Ponto inteiro:
public record Ponto(double x, double y) { }
A declaração entre parênteses é o cabeçalho do record, e seus elementos são os componentes. A partir dela, o compilador gera automaticamente:
- um campo
private finalpara cada componente; - um construtor canônico, que recebe todos os componentes na ordem declarada;
- um método de acesso para cada componente, com o nome do próprio componente:
x()ey(), sem o prefixoget; equalsehashCodebaseados em todos os componentes, seguindo o contrato de "Iguais ou o Mesmo?";- um
toStringlegível.
Ponto a = new Ponto(3, 4);
Ponto b = new Ponto(3, 4);
IO.println(a); // Ponto[x=3.0, y=4.0]
IO.println(a.x() + " " + a.y()); // 3.0 4.0
IO.println(a.equals(b)); // true
IO.println(a == b); // false
IO.println(a.hashCode() == b.hashCode()); // true
O javap mostra o que o compilador produziu:
public final class Ponto extends java.lang.Record {
public Ponto(double, double);
public final java.lang.String toString();
public final int hashCode();
public final boolean equals(java.lang.Object);
public double x();
public double y();
}
Um record é uma classe final que estende java.lang.Record. Tudo o que "O que Fica Dentro, o que Fica Fora" e "Iguais ou o Mesmo?" ensinaram sobre classes de valor está ali, gerado corretamente, sem chance de esquecer um campo no hashCode.
O que um record pode e não pode ter
Um record não é só uma estrutura de dados. Ele pode ter métodos, métodos estáticos, constantes e implementar interfaces:
public record Ponto(double x, double y) {
public static final Ponto ORIGEM = new Ponto(0, 0);
public Ponto deslocar(double dx, double dy) {
return new Ponto(x + dx, y + dy);
}
public double distanciaAte(Ponto outro) {
return Math.hypot(x - outro.x, y - outro.y);
}
}
new Ponto(3, 4).distanciaAte(Ponto.ORIGEM) dá 5.0. Math.hypot calcula a hipotenusa, √(dx² + dy²), sem risco de overflow nos quadrados.
As restrições derivam do propósito de representar um valor imutável e transparente:
- Os componentes são
final. Um método que tente alterá-los não compila:cannot assign a value to final variable x. Operações que "modificam" devolvem um record novo, comodeslocar. - Não pode haver campos de instância além dos componentes. O estado de um record é exatamente o que o cabeçalho declara:
field declaration must be static. Camposstaticsão permitidos. - Um record não pode estender outra classe, porque já estende
Record, e não pode ser estendido, porque éfinal. Pode implementar quantas interfaces quiser.
Records são ideais para os objetos de valor da aula anterior e para agrupar dados que viajam juntos: o resultado de uma consulta, uma mensagem entre partes do programa, um par de valores devolvido por um método. Não servem para entidades com estado mutável, como a ContaBancaria.
O construtor compacto: validando na entrada
Um record também precisa garantir as próprias invariantes. Para isso existe o construtor compacto, que valida os argumentos sem repetir a lista de parâmetros nem as atribuições:
public record Ponto(double x, double y) {
public Ponto {
if (Double.isNaN(x) || Double.isNaN(y)) {
throw new IllegalArgumentException("Coordenadas não podem ser NaN");
}
}
}
O construtor compacto não tem parênteses. Ele executa antes das atribuições, que o compilador acrescenta automaticamente no fim. Dentro dele, x e y são os parâmetros, e ele pode até reatribuí-los para normalizar os valores antes que sejam guardados:
record Intervalo(int inicio, int fim) {
Intervalo {
if (inicio > fim) {
int temp = inicio;
inicio = fim;
fim = temp;
}
}
}
new Intervalo(10, 3) produz Intervalo[inicio=3, fim=10]. Da mesma forma, record Nome(String valor) { Nome { valor = valor.strip(); } } guarda os nomes sem espaços nas pontas.
Um record pode ter outros construtores, que precisam delegar ao canônico com this(...), como em "Moldes e Instâncias".
A imutabilidade é rasa
Um cuidado importante. Os componentes são final, mas final protege a referência, e não o objeto apontado. Com um componente mutável, como um array, o record deixa de ser imutável na prática:
record Turma(String nome, String[] alunos) { }
String[] alunos = {"Ana", "Bia"};
Turma t = new Turma("A", alunos);
alunos[0] = "Zeca";
IO.println(t.alunos()[0]); // Zeca
IO.println(t); // Turma[nome=A, alunos=[Ljava.lang.String;@79ad8b2f]
É a mesma lição de "O que Fica Dentro, o que Fica Fora", agora no record. E há dois problemas a mais: o toString gerado mostra a referência do array, e o equals gerado compara arrays com equals, que para arrays é identidade. Duas turmas com os mesmos alunos em arrays diferentes são "diferentes". A solução é usar, nos componentes, tipos que já sejam imutáveis. Em vez de arrays, as listas imutáveis que você vai conhecer em "Listas, Conjuntos e Mapas". Até lá, prefira componentes imutáveis: números, String, outros records e enums.
Records locais
Um record pode ser declarado dentro de um método, quando só é útil ali:
void main() {
record Placar(String time, int gols) { }
Placar[] jogo = {new Placar("Azul", 2), new Placar("Verde", 1)};
for (Placar p : jogo) {
IO.println(p.time() + ": " + p.gols());
}
}
É uma forma prática de dar nome a um grupo de valores temporário sem criar um arquivo novo nem poluir o resto do programa.
Enums: um conjunto fixo de valores
Muitos dados só podem assumir um punhado de valores conhecidos: os naipes de um baralho, os dias da semana, os estados de um pedido. Representá-los com String ou int é frágil. Um "PAGO" digitado como "PAGU" compila e só falha na execução, e nada impede que o status receba o número 42. Um enum declara o tipo e todos os seus valores possíveis:
enum Naipe { OUROS, ESPADAS, COPAS, PAUS }
Naipe n = Naipe.COPAS;
IO.println(n); // COPAS
IO.println(n.name() + " " + n.ordinal()); // COPAS 2
IO.println(n == Naipe.COPAS); // true
for (Naipe naipe : Naipe.values()) {
IO.print(naipe + " "); // OUROS ESPADAS COPAS PAUS
}
IO.println("");
IO.println(Naipe.valueOf("PAUS")); // PAUS
Uma variável do tipo Naipe só pode conter um dos quatro valores, ou null, e o compilador garante isso. Cada constante é um objeto único: existe exatamente uma instância de COPAS em todo o programa. Por isso, enums podem ser comparados com == com toda a segurança, a exceção à regra de "Iguais ou o Mesmo?".
Os métodos que todo enum ganha:
values()devolve um array com todas as constantes, na ordem de declaração.valueOf(String)converte um texto na constante correspondente, e é sensível a maiúsculas.Naipe.valueOf("paus")lançaIllegalArgumentException: No enum constant ... paus.name()devolve o nome da constante, eordinal()devolve a posição dela, começando em zero.
Um aviso sobre o ordinal(): não guarde nem dependa dele. Se alguém inserir uma constante nova no meio da declaração, todas as posições seguintes mudam. Um sistema que tivesse gravado ordinais num banco de dados passaria a ler estados trocados. Para persistir enums, use o name() ou um código próprio, como um campo, a técnica mostrada a seguir.
Enums e switch: exaustividade de graça
O switch com setas de "O Programa que Escolhe e Repete" combina perfeitamente com enums:
String cor = switch (n) {
case OUROS, COPAS -> "vermelho";
case ESPADAS, PAUS -> "preto";
};
Não há default, e não precisa haver. O compilador conhece todas as constantes do enum e verifica que todas foram cobertas. Isso tem uma consequência valiosa: se amanhã alguém acrescentar uma constante ao enum, todo switch expression que não a trata deixa de compilar:
enum Semaforo { VERDE, AMARELO, VERMELHO }
String acao = switch (s) {
case VERDE -> "siga";
case AMARELO -> "atenção";
};
SwE.java:4: error: the switch expression does not cover all possible input values
O compilador aponta cada lugar do programa que precisa decidir o que fazer com o valor novo. Com um default, esses lugares cairiam silenciosamente no caso genérico. Por isso, em switch sobre enums, evite o default sempre que tratar todos os casos for possível. Ele troca uma verificação do compilador por um comportamento que talvez não seja o desejado.
Enums com campos, construtores e métodos
Aqui os enums do Java se afastam dos de C: como cada constante é um objeto, o enum pode ter campos, um construtor e métodos. Os estados de um pedido de loja virtual:
public enum StatusPedido {
AGUARDANDO_PAGAMENTO("Aguardando pagamento", false),
PAGO("Pago", false),
ENVIADO("Enviado", false),
ENTREGUE("Entregue", true),
CANCELADO("Cancelado", true);
private final String rotulo;
private final boolean finalizado;
StatusPedido(String rotulo, boolean finalizado) {
this.rotulo = rotulo;
this.finalizado = finalizado;
}
public String getRotulo() {
return rotulo;
}
public boolean isFinalizado() {
return finalizado;
}
public boolean podeIrPara(StatusPedido proximo) {
return switch (this) {
case AGUARDANDO_PAGAMENTO -> proximo == PAGO || proximo == CANCELADO;
case PAGO -> proximo == ENVIADO || proximo == CANCELADO;
case ENVIADO -> proximo == ENTREGUE;
case ENTREGUE, CANCELADO -> false;
};
}
}
Os argumentos entre parênteses em cada constante vão para o construtor, que é sempre privado, de forma implícita: ninguém de fora pode criar novos valores do enum. O rótulo para exibição fica separado do nome técnico da constante, e a regra de quais transições são permitidas fica no próprio tipo, e não espalhada pelo programa. Uma classe Pedido usa essa regra para se proteger:
public class Pedido {
private StatusPedido status = StatusPedido.AGUARDANDO_PAGAMENTO;
public void avancarPara(StatusPedido novo) {
if (!status.podeIrPara(novo)) {
throw new IllegalStateException("Transição inválida: " + status + " -> " + novo);
}
status = novo;
}
public StatusPedido getStatus() {
return status;
}
}
Pedido p = new Pedido();
p.avancarPara(StatusPedido.PAGO);
p.avancarPara(StatusPedido.ENVIADO);
IO.println(p.getStatus().getRotulo() + ", finalizado? " + p.getStatus().isFinalizado());
p.avancarPara(StatusPedido.CANCELADO);
Enviado, finalizado? false
Exception in thread "main" java.lang.IllegalStateException: Transição inválida: ENVIADO -> CANCELADO
Um pedido já enviado não pode ser cancelado, e nenhum trecho do programa consegue fazer isso por engano. O IllegalStateException é a exceção padrão para "a operação é válida em geral, mas não no estado atual do objeto", o irmão do IllegalArgumentException de "O que Fica Dentro, o que Fica Fora".
Um comportamento diferente em cada constante
Quando cada constante precisa de um comportamento próprio, o enum pode declarar um método abstrato, e cada constante o implementa num bloco:
enum Operacao {
SOMA("+") {
@Override
double aplicar(double a, double b) { return a + b; }
},
SUBTRACAO("-") {
@Override
double aplicar(double a, double b) { return a - b; }
},
MULTIPLICACAO("*") {
@Override
double aplicar(double a, double b) { return a * b; }
},
DIVISAO("/") {
@Override
double aplicar(double a, double b) { return a / b; }
};
private final String simbolo;
Operacao(String simbolo) {
this.simbolo = simbolo;
}
abstract double aplicar(double a, double b);
static Operacao porSimbolo(String simbolo) {
for (Operacao op : values()) {
if (op.simbolo.equals(simbolo)) {
return op;
}
}
throw new IllegalArgumentException("Operação desconhecida: " + simbolo);
}
}
for (Operacao op : Operacao.values()) {
IO.println("8 " + op.name() + " 2 = " + op.aplicar(8, 2));
}
IO.println(Operacao.porSimbolo("*").aplicar(6, 7)); // 42.0
8 SOMA 2 = 10.0
8 SUBTRACAO 2 = 6.0
8 MULTIPLICACAO 2 = 16.0
8 DIVISAO 2 = 4.0
42.0
É o polimorfismo de "Um Tipo, Várias Formas" dentro de um único tipo: cada constante é, por baixo, uma pequena subclasse anônima do enum, e op.aplicar é despachado para a versão dela. Uma operação nova, como a potência, é uma constante nova, e o compilador obriga a implementar aplicar. Qual das duas técnicas usar? O switch dentro do método, como em podeIrPara, é mais legível quando a lógica é uma tabela curta. Os corpos por constante são melhores quando cada comportamento é substancial e independente.
EnumSet e EnumMap
A biblioteca oferece duas coleções especializadas em enums, muito eficientes, porque por dentro usam bits e arrays indexados pelo ordinal:
enum Dia { SEG, TER, QUA, QUI, SEX, SAB, DOM }
EnumSet<Dia> fimDeSemana = EnumSet.of(Dia.SAB, Dia.DOM);
EnumSet<Dia> uteis = EnumSet.complementOf(fimDeSemana);
IO.println(uteis); // [SEG, TER, QUA, QUI, SEX]
EnumMap<Dia, Integer> horas = new EnumMap<>(Dia.class);
horas.put(Dia.SEG, 8);
horas.put(Dia.SEX, 6);
IO.println(horas); // {SEG=8, SEX=6}
Conjuntos e mapas em geral são o assunto de "Listas, Conjuntos e Mapas". Por ora, fica a indicação: quando os elementos ou as chaves forem enums, estas são as versões certas.
Records, enums e o que vem pela frente
Records e enums têm algo em comum. Os dois são tipos em que o Java sabe exatamente o que existe: um record é a soma dos seus componentes, e um enum é a lista das suas constantes. Esse conhecimento permite ao compilador gerar código, como o equals dos records, e verificar a exaustividade, como no switch sobre enums.
Falta uma peça. Um enum lista valores fixos, mas cada valor é um objeto único, sem dados próprios variáveis. E se quiséssemos um conjunto fechado de tipos, cada um com dados diferentes, como formas geométricas em que o círculo tem raio e o retângulo tem largura e altura, com a mesma garantia de que o compilador conhece todos os casos? É o que as sealed classes oferecem. Combinadas com records e com o pattern matching, elas permitem escrever um switch sobre tipos tão seguro quanto o switch sobre enums.
Fontes e leituras recomendadas
- JEP 395, "Records": https://openjdk.org/jeps/395. A proposta que tornou os records definitivos no Java 16, com a motivação, as restrições e a explicação do construtor compacto.
- Brian Goetz, "Data Classes and Sealed Types for Java" (2019): https://cr.openjdk.org/~briangoetz/amber/datum.html. O documento de projeto que explica por que os records são "portadores transparentes de dados", e não apenas um atalho para escrever menos.
- The Java Language Specification, Java SE 25, seção 8.9, "Enum Classes": https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.9. As regras completas dos enums, inclusive os corpos por constante.
- Joshua Bloch, Effective Java, 3ª edição, itens 34 a 38: "Use enums instead of int constants", "Use instance fields instead of ordinals", "Use EnumSet instead of bit fields", "Use EnumMap instead of ordinal indexing" e os padrões com enums. O exemplo da
Operacaocom corpos por constante vem do item 34. - Dev.java, "Using Record to Model Immutable Data": https://dev.java/learn/records/. O tutorial oficial sobre records, com exemplos de construtores compactos e records locais.
Exercícios
Exercício 1
Reescreva a classe imutável Retangulo do exercício 3 de "O que Fica Dentro, o que Fica Fora" como um record, mantendo a validação, os métodos area(), perimetro() e ehQuadrado(), e o redimensionar(double fator).
Ver resposta
✓ Resposta:
public record Retangulo(double largura, double altura) {
public Retangulo {
if (largura <= 0 || altura <= 0) {
throw new IllegalArgumentException(
"Dimensões precisam ser positivas: " + largura + " x " + altura);
}
}
public double area() {
return largura * altura;
}
public double perimetro() {
return 2 * (largura + altura);
}
public boolean ehQuadrado() {
return largura == altura;
}
public Retangulo redimensionar(double fator) {
return new Retangulo(largura * fator, altura * fator);
}
}
Das cerca de 30 linhas da versão original, sobraram as que dizem algo: a validação e as operações. Campos, construtor, métodos de acesso, equals, hashCode e toString são gerados. A validação no construtor compacto vale para toda criação, inclusive a feita por redimensionar. Os métodos de acesso agora se chamam largura() e altura(), e não getLargura() e getAltura(): é a convenção dos records, e o código que usava a classe antiga precisaria ser ajustado.
Exercício 2
O record Turma(String nome, String[] alunos) da aula tem três problemas ligados ao array. Liste-os e mostre como um construtor compacto e um método de acesso sobrescrito resolveriam dois deles. Qual continua sem solução?
Ver resposta
✓ Resposta: Os problemas: (a) quem criou a turma pode alterar o array original depois, e a turma muda; (b) quem chama alunos() recebe o array interno e pode alterá-lo; (c) o equals e o hashCode gerados comparam o array por identidade, então turmas com os mesmos alunos em arrays diferentes são diferentes (o toString também mostra a referência). Os dois primeiros se resolvem com cópias defensivas:
record Turma(String nome, String[] alunos) {
Turma {
alunos = alunos.clone(); // copia na entrada
}
@Override
public String[] alunos() {
return alunos.clone(); // copia na saída
}
}
O terceiro continua, a menos que se sobrescrevam também equals, hashCode e toString usando Arrays.equals, Arrays.hashCode e Arrays.toString, o que desfaz boa parte da vantagem do record. A solução real é não usar um tipo mutável como componente: com uma lista imutável, de "Listas, Conjuntos e Mapas", os três problemas desaparecem sem nenhum código extra.
Exercício 3
Crie um enum DiaDaSemana com as sete constantes, um método isFimDeSemana() e um método proximo() que devolva o dia seguinte, com domingo seguido de segunda.
Ver resposta
✓ Resposta:
enum DiaDaSemana {
SEGUNDA, TERCA, QUARTA, QUINTA, SEXTA, SABADO, DOMINGO;
boolean isFimDeSemana() {
return this == SABADO || this == DOMINGO;
}
DiaDaSemana proximo() {
DiaDaSemana[] dias = values();
return dias[(ordinal() + 1) % dias.length];
}
}
DiaDaSemana.DOMINGO.proximo() devolve SEGUNDA, graças ao %, que dá a volta como em "Operadores, Texto e Conversa com o Usuário". Aqui o uso do ordinal() é legítimo, porque ele é usado só em memória, para navegar na ordem declarada, e nunca é gravado. Se a ordem das constantes mudasse, proximo() acompanharia automaticamente. isFimDeSemana usa ==, correto para enums. A biblioteca padrão tem um enum pronto com essa mesma ideia, java.time.DayOfWeek, com o método plus(1), que você vai usar em "Tempo, Dinheiro e Padrões".
Exercício 4
Um sistema grava o status dos pedidos no banco de dados usando status.ordinal(). Explique o que acontece se um desenvolvedor acrescentar a constante EM_SEPARACAO entre PAGO e ENVIADO, e proponha uma forma de persistência robusta.
Ver resposta
✓ Resposta: Com EM_SEPARACAO inserida na terceira posição, os ordinais de ENVIADO, ENTREGUE e CANCELADO passam de 2, 3 e 4 para 3, 4 e 5. Os registros já gravados continuam com os números antigos. Um pedido gravado como 2 (enviado) passa a ser lido como EM_SEPARACAO, um pedido entregue (3) passa a ser lido como enviado, e assim por diante. Os dados ficam corrompidos em silêncio, sem nenhum erro, e o problema pode levar semanas para ser percebido. Formas robustas: gravar o name(), que não depende da ordem (mas quebra se a constante for renomeada), ou, melhor ainda, dar a cada constante um código explícito e estável num campo, como PAGO("PG"), com um método estático que converte o código de volta na constante, no estilo do porSimbolo da Operacao. O código gravado não muda nem com a reordenação nem com a renomeação das constantes.
Exercício 5
O banco quer acrescentar o estado DEVOLVIDO ao StatusPedido, permitido apenas a partir de ENTREGUE. Liste o que precisa ser alterado e explique como o compilador ajuda a não esquecer nada.
Ver resposta
✓ Resposta: É preciso (1) acrescentar a constante DEVOLVIDO("Devolvido", true), e (2) atualizar podeIrPara: o caso ENTREGUE passa a devolver proximo == DEVOLVIDO, e o novo caso DEVOLVIDO devolve false. O compilador ajuda porque podeIrPara usa um switch expression sem default. Assim que a constante é acrescentada, esse switch deixa de compilar com "does not cover all possible input values", e o mesmo acontece com todo switch expression sem default sobre StatusPedido em qualquer parte do programa, como um que escolhe a cor de exibição de cada status. Cada erro aponta um lugar que precisa de uma decisão sobre o estado novo. Se esses switch tivessem default, o programa compilaria, e DEVOLVIDO cairia no comportamento genérico sem que ninguém pensasse sobre ele. É o motivo prático para evitar o default em switch sobre enums.