Record Patterns – Java 21

A versão 21 do Java foi aprimorada com padrões de registro (Record Patterns) para desconstruir valores de um objeto record. Padrões de registro chegou a linguagem para permitir uma forma poderosa, declarativa e combinável de navegação e processamento de dados.

Mas antes de abordar Record Patterns, vamos relembrar o que é um record.

O que é um record?

No Java 16 foi lançado o recurso record, uma maneira mais concisa e expressiva de definir classes que armazenam dados. Vamos analisar a diferença entre o código criado com base em uma classe e um record.

Quando criamos uma classe, precisamos definir explicitamente os atributos, métodos getters, setters e, se necessário, um método construtor com os argumentos referentes aos atributos da classe. A seguir temos o código da classe Person que exemplifica isso:

public class Person {
    private String firstName;
    private String lastName;
    private int age;

    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public int getAge() {
        return age;
    }
}

Contudo, com o recurso record temos a possibilidade de reduzir drasticamente a quantidade de código apresentado na classe Person:

public record Person(String firstName, String lastName, int age) {
}

Com isso, o record Person já possui automaticamente:

  • Um construtor que inicializa os campos.
  • Métodos acessores (getters) para cada campo (firstName()lastName()age()).
  • Implementações padrão dos métodos equals()hashCode() e toString().

Embora se possa perceber a redução no código padrão, a verdadeira intenção dos records é fornecer uma forma transparente de definir dados. Isto é permitido através da colocação de diversas restrições nos registros, incluindo:

  • Todos os campos são final
  • As classes de registro são implicitamente final
  • Campos de declaração de instância são proibidos

Além disso, o record também fornece alguns recursos:

  • Todas as classes de record incluem construtores canônicos (default)
  • Todas as classes de record incluem acessadores de componentes

Dessa forma, podemos observar alguns conceitos importantes sobre o uso de record:

Imutabilidade: Os records são imutáveis, o que significa que seus campos não podem ser alterados após a criação do objeto.

Concisão: A definição de um record é mais enxuta, pois ele encapsula automaticamente os detalhes comuns de uma classe de dados.

Menos boilerplate: Com records, você economiza tempo e linhas de código, evitando repetições.

Em resumo, se você precisa apenas armazenar dados e não precisa de mutabilidade, os records são uma escolha excelente e mais elegante no Java. Essas restrições e recursos tornaram os registros candidatos ideais para adicionar desconstrutores.

Padrões de Registros (Record Patterns)

Os padrões de registro foram propostos como um recurso de visualização pelo JEP 405 e entregues no JDK 19 , e visualizados uma segunda vez pelo JEP 432 e entregues no JDK 20 . No JDK 21 foi marcado com status de entregue pela JEP 440.

Record Patterns é um recurso introduzido no Java 19 que permite desmontar (ou desconstruir) valores de registros (records). Ele serve para combinar padrões de correspondência com registros, facilitando a extração de componentes dos registros e a navegação em dados de maneira declarativa. Com o Record Patterns, podemos verificar se um valor corresponde a um tipo de registro específico e vincular variáveis aos componentes desse registro. Isso torna o código mais conciso e legível, especialmente quando trabalhamos com registros aninhados ou genéricos.

Desconstrutores e Component Matching

Os desconstrutores, conforme o próprio nome sugere, representam o inverso dos construtores. Enquanto um construtor se encarrega de gerar e adicionar dados a um objeto, um desconstrutor foca em extrair informações dele, tal como demonstrado neste caso:

public record Vehicle(String brand, String model, String licensePlate) {

}

class Main {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle("Honda", "Fit 1.4", "JAV-1621");

        if(vehicle instanceof Vehicle(String brand, String model, String licensePlate)) {
            System.out.println(brand + " - " + model + ": " + licensePlate);
        }
    }
}

Observe no código acima, o qual temos o record Vehicle. No método principal da classe Main foi adicionada uma instancia para criar o objeto com a definição de seus valores. Na linha abaixo, temos um teste condicional que verifica se o objeto é do tipo Vehicle usando Component Matching com o operador instanceof.

O Component Matching ou componente de correspondência (String brand, String model, String licensePlate) é usado para extrair os valores dos campos do objeto Vehicle. Se o objeto for uma instância de Vehicle, os valores dos campos serão extraídos e o bloco de código dentro do if será executado.

O conceito de desconstrutor é usado acessando diretamente os argumentos do método construtor na linha com o System.out. Ou seja, o objeto é desconstruído para se ter acesso aos valores de seus atributos.

Além disso, com o componente de correspondência, também chamado de padrão de correspondência, o tipo de uma variável só precisa ser compatível com o aquilo que foi declarado no componente record. No exemplo abaixo, var é usado em substituição aos tipos String e int e ainda assim, o teste condicional será verdadeiro.

if(vehicle instanceof Vehicle(var marca, var modelo, var placa)) {
   System.out.println(marca + " - " + modelo + ": " + placa);
}

Outro ponto importante a ser observado está relacionado aos nomes dos argumentos do construtor de Vehicle. Veja que os nomes foram alterados na declaração do método, na linha do teste condicional, e mesmo assim a o objeto será desconstruído e os valores de seus atributos estarão acessíveis no System.out.

Você pode usar as técnicas de padrões de registro no switch/case, que agora oferece suporte à correspondência de padrões com Java 21. Veja no código apresenta um exemplo de como proceder:

public interface Interest {}

public record SimpleInterest(double principal, double rate, int time) implements Interest {}

public record CompoundInterest(double principal, double rate, int time) implements Interest {}

public class Main {

    public static void main(String[] args) {
        var res1 = calculate(new SimpleInterest(1000.0, 3.5, 2));
        System.out.println(res1);

        var res2 = calculate(new CompoundInterest(580.0, 5.5, 3));
        System.out.println(res2);
    }

    static Object calculate(Interest interest) {
        return switch(interest) {
            case SimpleInterest(var p, var r, var t) -> p * r * t / 100;

            case CompoundInterest(var p, var r, var t) -> p * Math.pow(1 + r / 100, t) - p;

            default -> "Invalid interest type.";
        };
    }
}

O código apresenta a interface Interest que é implementada pelos records SimpleInterest e CompoundInterest. No método de execução criamos a instancia para os objetos SimpleInterest e CompoundInterest. Essas instancias são adicionadas como parâmetro do método estático calculate().

Em calculate() temos o bloco switch, onde as linhas case retornarão o resultado dos cálculos para cada tipo de objeto. Se o objeto de instancia for SimpleInterest o primeiro case será verdadeiro, caso o objeto de instancia armazenado na variável interest seja CompoundInterest, então o segundo case será verdadeiro.

Observe que a variável interest é diretamente comparada em cada um dos case, como se existisse explicitamente um teste do tipo instance of participando desta operação. Quando os tipos forem os mesmo, então o objeto declarado no case será desconstruído e suas variáveis preenchidas com os valores contidos na variável interest. Podendo assim, usá-las como partes do calculo que vem a seguir.

Conclusão

Em resumo, record e padrões de registro são fundamentais na evolução da programação orientada a dados no Java. Esta narrativa está em constante desenvolvimento, como exemplificado pelo JEP 443, que trouxe padrões e variáveis nomeadas como um recurso visual no Java 21. Recomenda-se consultar a seção de leitura adicional do artigo de Brian Goetz, “Data-Oriented Programming in Java“, para compreender melhor como isso será implementado.

Referencias

Ballem

Marcio Ballem é bacharel em Sistemas de Informação pelo Centro Universitário Franciscano em Santa Maria/RS. Tem experiência com desenvolvimento Delphi e Java em projetos para gestão pública e acadêmica. Possui certificação em Java, OCJP 6.

Você pode gostar...