Pattern Matching para Tipos em Java
O Pattern Matching para tipos (type pattern) é um recurso que permite verificar se uma variável ou o resultado de uma expressão corresponde a um tipo específico. Em Java, isso pode ser feito com o operador instanceof ou com expressões switch.
Para ilustrar os exemplos, vamos considerar a seguinte estrutura de classes:
// Classe para representar uma pessoa
class Person {
private String name;
String getName() {
return name;
}
Person(String name) {
this.name = name;
}
}
// Classe para um funcionário de uma empresa
class Employee extends Person {
private String company; // Empresa onde a pessoa trabalha
String getCompany() {
return company;
}
Employee(String name, String company) {
super(name);
this.company = company;
}
}
// Classe para um cliente de um banco
class Client extends Person {
private String bank; // Banco do cliente
String getBank() {
return bank;
}
Client(String name, String bank) {
super(name);
this.bank = bank;
}
}O Operador instanceof
Tradicionalmente, o operador instanceof era usado apenas para verificar se um objeto pertencia a um determinado tipo. Nas versões mais recentes do Java, sua funcionalidade foi expandida para, além de realizar a verificação, declarar uma nova variável do tipo correspondente, conhecida como variável de padrão.
Observe o exemplo a seguir, que demonstra o uso do instanceof com o Pattern Matching:
class Program {
public static void main(String[] args) {
Person tom = new Person("Tom");
Person bob = new Employee("Bob", "Google");
Person sam = new Client("Sam", "Sberbank");
printPerson(tom); // Person Tom
printPerson(bob); // Bob works in Google
printPerson(sam); // Sam is client of Sberbank
}
static void printPerson(Person person) {
if (person instanceof Employee empl) {
System.out.printf("%s works in %s\n", empl.getName(), empl.getCompany());
} else if (person instanceof Client cl) {
System.out.printf("%s is client of %s\n", cl.getName(), cl.getBank());
} else {
System.out.println("Person " + person.getName());
}
}
}No método printPerson, a expressão person instanceof Employee empl verifica se o objeto person é uma instância da classe Employee. Se a verificação for verdadeira, o objeto é automaticamente convertido para o tipo Employee e atribuído à variável empl. Essa variável fica disponível dentro do bloco if, permitindo o acesso a métodos específicos da classe Employee, como getCompany(). O mesmo princípio se aplica à verificação do tipo Client.
O Pattern Matching com instanceof também pode ser utilizado em operadores ternários para simplificar o código.
class Program {
public static void main(String[] args) {
Person kate = new Client("Kate", "Sberbank");
printEmplCompany(kate); // Imprime uma linha em branco
Person sam = new Employee("Sam", "Google");
printEmplCompany(sam); // Google
Person bob = new Employee("Bob", "VK");
printEmplCompany(bob); // VK
}
static void printEmplCompany(Person p) {
String company = (p instanceof Employee empl) ? empl.getCompany() : "";
System.out.println(company);
}
}Expressões switch
As expressões switch foram aprimoradas para suportar o Pattern Matching, tornando o código de verificação de tipos mais conciso e legível em comparação com uma cadeia de if-else if.
class Program {
public static void main(String[] args) {
Person tom = new Person("Tom");
Person bob = new Employee("Bob", "Google");
Person sam = new Client("Sam", "Sberbank");
printPerson(tom); // Person Tom
printPerson(bob); // Bob works in Google
printPerson(sam); // Sam is client of Sberbank
}
static void printPerson(Person person) {
switch (person) {
case Employee empl ->
System.out.printf("%s works in %s\n", empl.getName(), empl.getCompany());
case Client cl ->
System.out.printf("%s is client of %s\n", cl.getName(), cl.getBank());
default ->
System.out.println("Person " + person.getName());
}
}
}Aqui, o rótulo case Employee empl funciona de forma semelhante ao instanceof. Ele testa se a variável person é do tipo Employee e, se for, atribui o objeto à variável empl.
Quando a variável de padrão não é necessária no bloco de código, é possível usar um sublinhado (_), conhecido como variável anônima. Isso indica que estamos interessados apenas no tipo do objeto, e não no próprio objeto.
static void printPerson(Person person) {
switch (person) {
case Employee _ ->
System.out.println("Person is an Employee");
case Client _ ->
System.out.println("Person is a Client");
default ->
System.out.println("Person " + person.getName());
}
}Tratamento de Valores Nulos
É importante considerar que a variável sendo testada pode ter o valor null.
instanceof
O operador instanceof lida com valores nulos de forma segura. Ele retorna false se a variável à sua esquerda for null, evitando a ocorrência de um NullPointerException. No exemplo abaixo, se person for null, ambas as verificações instanceof resultarão em false, e o código executará o bloco else.
class Program {
public static void main(String[] args) {
Person tom = null;
Person bob = new Employee("Bob", "Google");
printPerson(tom); // Person is Undefined
printPerson(bob); // Employee Bob
}
static void printPerson(Person person) {
if (person instanceof Employee) {
System.out.println("Employee " + person.getName());
} else if (person instanceof Person) {
System.out.println("Person " + person.getName());
} else { // Este bloco é executado se person for null
System.out.println("Person is Undefined");
}
}
}switch
Diferente do instanceof, uma expressão switch tradicional lançaria um NullPointerException se a variável testada fosse null. No entanto, as versões modernas do switch permitem tratar esse caso explicitamente com um case null.
class Program {
public static void main(String[] args) {
Person tom = null;
Person bob = new Employee("Bob", "Google");
printPerson(tom); // Person is Undefined
printPerson(bob); // Employee Bob
}
static void printPerson(Person person) {
switch (person) {
case null ->
System.out.println("Person is Undefined");
case Employee _ ->
System.out.println("Employee " + person.getName());
default ->
System.out.println("Person " + person.getName());
}
}
}Cláusulas de Guarda (Guards)
É possível adicionar condições extras aos padrões, conhecidas como cláusulas de guarda ou guards. Essas condições devem ser satisfeitas para que o bloco de código correspondente seja executado.
instanceof com Guarda
Com o instanceof, as guardas são adicionadas como expressões lógicas encadeadas com o operador &&.
class Program {
public static void main(String[] args) {
Person bob = new Employee("Bob", "Google");
Person sam = new Employee("Sam", "Sberbank");
printPerson(bob); // Employee Bob works in Google
printPerson(sam); // Employee Sam
}
static void printPerson(Person person) {
// A variável empl é usada na condição encadeada
if (person instanceof Employee empl && empl.getCompany().equals("Google")) {
System.out.println("Employee " + empl.getName() + " works in Google");
} else if (person instanceof Employee) {
System.out.println("Employee " + person.getName());
} else if (person instanceof Person) {
System.out.println("Person " + person.getName());
}
}
}A expressão empl.getCompany().equals("Google") é a cláusula de guarda. O bloco if só será executado se person for um Employee e, ao mesmo tempo, trabalhar na empresa "Google".
switch com Guarda
Nas expressões switch, as cláusulas de guarda são especificadas com a palavra chave when.
class Program {
public static void main(String[] args) {
Person bob = new Employee("Bob", "Google");
Person sam = new Employee("Sam", "Sberbank");
Person tom = new Person("Tom");
printPerson(bob); // Employee Bob works in Google
printPerson(sam); // Employee Sam
printPerson(tom); // Person Tom
}
static void printPerson(Person person) {
switch (person) {
case Employee empl when empl.getCompany().equals("Google") ->
System.out.println("Employee " + empl.getName() + " works in Google");
case Employee _ ->
System.out.println("Employee " + person.getName());
case Person _ ->
System.out.println("Person " + person.getName());
}
}
}Neste caso, a expressão when empl.getCompany().equals("Google") atua como a cláusula de guarda. As guardas podem conter condições mais complexas, combinando múltiplas verificações para refinar a lógica de correspondência. Por exemplo, o bloco case a seguir só é executado se o objeto for um Employee cujo nome é "Bob" e a empresa é "Google".
switch (person) {
case Employee empl
when empl.getCompany().equals("Google") && empl.getName().equals("Bob") ->
System.out.println("Bob works in Google");
// outros cases
}Resumo
- Pattern Matching: Permite testar se um objeto corresponde a um tipo e, opcionalmente, extrair seus dados em variáveis.
instanceofAprimorado: Além de verificar o tipo, declara e inicializa uma variável do tipo correspondente, que fica disponível no escopo doif.switchAprimorado: Suportacasecom tipos, tornando o código mais limpo e legível. Permite também o uso decase nullpara tratar valores nulos.- Variável Anônima (
_): Pode ser usada quando apenas a verificação do tipo é necessária, sem a utilização da variável de padrão. - Cláusulas de Guarda (
when): Permitem adicionar condições adicionais a umcasenoswitch, tornando a lógica de correspondência mais poderosa e específica.