Pattern Matching com Records em Java
A partir do Java 21, é possível utilizar o Record Pattern, um recurso que permite não apenas verificar se um objeto é uma instância de um record, mas também desestruturá-lo — um processo que extrai os valores de seus componentes diretamente em variáveis. Este padrão pode ser aplicado tanto com o operador instanceof quanto com expressões switch.
O Operador instanceof
O exemplo a seguir demonstra o uso do Record Pattern com o operador instanceof.
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", 22);
if (tom instanceof Person(var pName, var pAge)) {
System.out.printf("Person. Name: %s; Age: %d\n", pName, pAge);
}
}
}
record Person(String name, int age) {}A expressão tom instanceof Person(var pName, var pAge) realiza duas operações. Primeiro, verifica se a variável tom é uma instância do record Person. Se for, ela extrai os valores dos componentes name e age e os atribui às novas variáveis de padrão pName e pAge, que ficam disponíveis dentro do escopo do if.
Também é possível declarar os tipos das variáveis de forma explícita, em vez de usar var.
if (tom instanceof Person(String pName, int pAge)) {
System.out.printf("Person. Name: %s; Age: %d\n", pName, pAge);
}Se algum componente do record não for necessário, ele pode ser ignorado na desestruturação, substituindo a variável por um sublinhado (_), conhecido como variável anônima. Por exemplo, se apenas o componente age for relevante:
if (tom instanceof Person(_, int pAge)) {
System.out.printf("Person Age: %d\n", pAge);
}Expressões switch
O Record Pattern pode ser utilizado de forma elegante em expressões switch, tornando o código mais conciso e legível.
Person tom = new Person("Tom", 22);
switch (tom) {
case Person(var pName, var pAge) ->
System.out.printf("Person. Name: %s; Age: %d\n", pName, pAge);
case null ->
System.out.println("Undefined");
}Constantes e Cláusulas de Guarda (Guards)
É importante notar que o Record Pattern não permite o uso de valores constantes diretamente na desestruturação. O código a seguir, que tenta verificar se o nome da pessoa é "Tom" diretamente no padrão, resultará em um erro de compilação.
// Este código não compila!
if (tom instanceof Person("Tom", _)) { // Erro: "Tom" é uma constante
System.out.println("Person Tom");
}A abordagem correta para adicionar condições específicas é usar cláusulas de guarda. Com instanceof, isso é feito encadeando uma condição booleana com o operador &&.
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", 22);
Person bob = new Person("Bob", 46);
printPerson(tom); // Hello Tom
printPerson(bob); // Hello
}
static void printPerson(Person person) {
if (person instanceof Person(var pName, _) && pName.equals("Tom")) {
System.out.println("Hello Tom");
} else if (person instanceof Person(_, _)) {
System.out.println("Hello");
}
}
}Dessa forma, a condição pName.equals("Tom") atua como uma cláusula de guarda. O primeiro bloco if só é executado se o objeto for um Person e seu nome for "Tom".
Ao usar switch, as cláusulas de guarda são introduzidas pela palavra chave when.
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", 22);
Person bob = new Person("Bob", 46);
printPerson(tom); // Hello Tom
printPerson(bob); // Hello
}
static void printPerson(Person person) {
switch (person) {
// Apenas para um Person cujo name é "Tom"
case Person(var pName, _) when pName.equals("Tom") ->
System.out.println("Hello Tom");
// Para todos os outros objetos Person
case Person(_, _) ->
System.out.println("Hello");
case null -> {} // Se for null, não faz nada
}
}
}Padrões Aninhados
Os Record Patterns podem ser aninhados, permitindo desestruturar objetos complexos de forma declarativa e concisa.
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", 41);
Company mycorp = new Company("MyCorp", tom);
if (mycorp instanceof Company(_, Person(var name, _))) {
System.out.println("CEO: " + name); // CEO: Tom
}
}
}
record Person(String name, int age) {}
record Company(String name, Person ceo) {}Neste exemplo, o segundo componente do record Company é um record Person. A expressão instanceof aninha outro padrão para desestruturar o record Person interno, extraindo o valor do componente name.
O mesmo pode ser feito com uma expressão switch.
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", 41);
Company mycorp = new Company("MyCorp", tom);
switch (mycorp) {
case Company(_, Person(var name, _)) ->
System.out.println("CEO: " + name);
case null -> {}
default -> {}
}
}
}
record Person(String name, int age) {}
record Company(String name, Person ceo) {}Ao usar switch com Pattern Matching, a verificação deve ser exaustiva, o que significa que todos os valores possíveis para a variável de entrada devem ser tratados por um dos case. No exemplo acima:
case Company(_, Person(var name, _))trata o caso em quemycorpé umaCompanycom umceonão nulo.case nulltrata o caso em que a variávelmycorpé nula.defaulté necessário para tratar todas as outras possibilidades, como o caso em quemycorpé uma instância deCompany, mas seu componenteceoé nulo (new Company("MyCorp", null)). Este caso não corresponderia ao primeiro padrão.
Resumo
- Record Pattern: Permite verificar se um objeto é uma instância de um
recorde, ao mesmo tempo, extrair os valores de seus componentes para variáveis locais. - Desestruturação: Simplifica o acesso aos dados de um
record, eliminando a necessidade de chamar métodos de acesso. - Variável Anônima (
_): É usada para ignorar componentes dorecordque não são necessários na lógica do programa. - Cláusulas de Guarda (
when): Permitem adicionar condições lógicas aos padrões, refinando os critérios de correspondência. - Padrões Aninhados: Possibilitam a desestruturação de
recordsque contêm outrosrecordscomo componentes. - Exaustividade no
switch: É obrigatório que oscasede umswitchcom padrões cubram todas as possibilidades de valor da variável de entrada, incluindonull.