Java Pattern Matching
Pattern matching (em português, "casamento de padrões") é perguntar "este objeto tem esta forma?" e, na mesma tacada, pegar as partes dele para usar. Antes, o Java obrigava você a testar o tipo, converter e só depois usar, em três passos repetitivos. Do Java 16 ao 21, a linguagem ganhou um jeito curto e seguro de fazer tudo isso de uma vez.
- O
instanceofcom padrão Java 16+: testar o tipo e já ganhar uma variável. - Onde essa variável existe (e o erro
cannot find symbolquando você a usa fora do lugar). - O
switchcom padrões de tipo e a palavrawhenJava 21+. - Os record patterns Java 21+: desmontar um record dentro do padrão, inclusive um dentro do outro.
- Por que o compilador exige que o
switchcubra "todos os casos", como as classessealeddispensam odefaulte como tratar onullcomcase null.
Esta aula usa coisas que você já viu: polimorfismo (uma variável do tipo Object pode guardar um String, um Integer…), conversão de tipos (cast), o switch com setas e os records. Os exemplos rodaram no JDK 25 da máquina da Ana, na pasta ~/java.
A ideia antes do código
Imagine a Ana separando as encomendas da loja. Chega uma caixa fechada. Ela pergunta: "isto é uma caixa de frutas?". Se for, ela abre e pega as frutas. Se for uma caixa de livros, faz outra coisa. Repare que a pergunta e o "abrir" andam juntos: ninguém pergunta "é de frutas?", ouve "sim" e depois finge que não sabe e confere de novo.
No Java antigo era exatamente isso que acontecia. O programa perguntava o tipo com instanceof (em inglês, "é instância de", ou seja, "é um objeto do tipo…") e, mesmo depois de ouvir "sim", precisava converter o objeto para o tipo certo com um cast. Um padrão junta as duas coisas: "se for um String, chame-o de texto".
1. O jeito antigo: testar e converter
Uma variável do tipo Object aceita qualquer objeto. Só que, por ser genérica, ela não deixa você chamar métodos de String, como toUpperCase() (deixar em maiúsculas). Era preciso testar e converter:
public class Main {
public static void main(String[] args) {
Object coisa = "banana";
// Jeito antigo: testar, depois converter (cast)
if (coisa instanceof String) {
String texto = (String) coisa;
System.out.println("Antigo: " + texto.toUpperCase());
}
// Jeito novo (Java 16+): testar e já dar um nome
if (coisa instanceof String texto) {
System.out.println("Novo: " + texto.toUpperCase());
}
}
}
$ javac Main.java && java Main Antigo: BANANA Novo: BANANA
As duas formas fazem a mesma coisa. No jeito antigo, a palavra String aparece três vezes (no teste, na declaração e no cast), e nada impede você de errar uma delas, como testar String e converter para Integer. No jeito novo, aparece uma vez só.
2. instanceof com padrão Java 16+
- coisa
- O objeto que você quer examinar.
- instanceof
- "É um objeto do tipo…?". Responde
trueoufalse. - String
- O tipo que você está testando.
- texto
- A novidade: o nome de uma variável de padrão. Se o teste der
true, ela nasce já do tipoString, guardando o mesmo objeto. Não tem cast.
O conjunto String texto é o padrão de tipo: um tipo seguido de um nome. Ele "casa" com o objeto quando o objeto é daquele tipo. Com vários else if, você trata cada tipo do seu jeito:
public class Main {
static void descrever(Object item) {
if (item instanceof String s) {
System.out.println("Texto com " + s.length() + " letras: " + s);
} else if (item instanceof Integer n) {
System.out.println("Número inteiro; o dobro é " + (n * 2));
} else if (item instanceof Double d) {
System.out.println("Número decimal; metade é " + (d / 2));
} else {
System.out.println("Não sei descrever: " + item);
}
}
public static void main(String[] args) {
descrever("maçã");
descrever(21);
descrever(7.5);
descrever(true);
}
}
$ javac Main.java && java Main Texto com 4 letras: maçã Número inteiro; o dobro é 42 Número decimal; metade é 3.75 Não sei descrever: true
Cada chamada caiu num ramo diferente. O 21 virou um Integer por autoboxing, casou com Integer n, e dentro do ramo n é um número de verdade: dá para fazer n * 2. O true não casou com nenhum padrão e foi para o else.
Onde a variável de padrão existe
A variável s só existe nos lugares em que o Java tem certeza de que o teste deu certo. Essa regra tem até nome: flow scoping ("escopo pelo fluxo"). Na prática:
- Dentro do
ifque testou: existe. - Depois de um
&&(e): existe, porque o lado direito só roda se o esquerdo deutrue. Entãoo instanceof String s && s.length() > 3funciona. - Depois de um
||(ou): não existe, porque o lado direito roda justamente quando o teste deufalse. - Com a negação
!(… instanceof …)e umreturn: a variável existe depois doif, porque só chega lá quem passou no teste.
public class Main {
static void boasVindas(Object nome) {
if (!(nome instanceof String s)) {
System.out.println("Isso não é um nome.");
return;
}
// daqui para baixo o Java sabe que nome é String: s existe
System.out.println("Olá, " + s + "!");
}
public static void main(String[] args) {
Object[] itens = { "Ana", 42, "kiwi", 3.5 };
for (Object o : itens) {
if (o instanceof String s && s.length() > 3) {
System.out.println("Palavra longa: " + s);
}
}
boasVindas("Ana");
boasVindas(10);
}
}
$ javac Main.java && java Main Palavra longa: kiwi Olá, Ana! Isso não é um nome.
"Ana" é um String, mas tem só 3 letras, então o && deu false; 42 e 3.5 nem são textos. Só "kiwi" passou nos dois testes. Na boasVindas, o return faz o método terminar cedo para quem não é texto; quem continua é String, e o Java deixa usar s.
Missão: o vetor itens mistura textos e números. Use instanceof com padrão (o instanceof Integer n) para somar só os inteiros e mostrar Soma: 49.
public class Main {
public static void main(String[] args) {
Object[] itens = { "Ana", 42, "kiwi", 7, 2.5 };
int soma = 0;
for (Object o : itens) {
// teste aqui se o é um Integer e some
}
System.out.println("Soma: " + soma);
}
}
public class Main {
public static void main(String[] args) {
Object[] itens = { "Ana", 42, "kiwi", 7, 2.5 };
int soma = 0;
for (Object o : itens) {
if (o instanceof Integer n) {
soma = soma + n;
}
}
System.out.println("Soma: " + soma);
}
}
3. switch com padrões de tipo Java 21+
Uma escada de if … else if testando tipos fica comprida. Desde o Java 21, o switch com setas aceita padrões no lugar de valores fixos. Cada case diz um tipo e um nome:
public class Main {
static String descrever(Object item) {
return switch (item) {
case Integer n -> "inteiro " + n;
case Double d -> "decimal " + d;
case String s -> "texto \"" + s + "\"";
default -> "outra coisa: " + item;
};
}
public static void main(String[] args) {
System.out.println(descrever(10));
System.out.println(descrever(2.5));
System.out.println(descrever("pera"));
System.out.println(descrever('x'));
}
}
$ javac Main.java && java Main inteiro 10 decimal 2.5 texto "pera" outra coisa: x
| Linha | O que faz |
|---|---|
return switch (item) { | Um switch que produz um valor (uma switch expression); o return devolve esse valor. |
case Integer n -> … | Se item for um Integer, chame-o de n e produza o texto da direita. |
case String s -> … | Mesmo raciocínio para String. Os case são testados de cima para baixo. |
default -> … | Para todo o resto (aqui, o char 'x', que virou um Character). |
Condições extras com when
Às vezes o tipo não basta: você quer "um inteiro maior ou igual a 7". Para isso existe a guarda when (inglês para "quando"): depois do padrão, uma condição que também precisa ser verdadeira.
public class Main {
static String classificar(Object nota) {
return switch (nota) {
case Integer n when n >= 7 -> "aprovado com " + n;
case Integer n when n >= 5 -> "recuperação com " + n;
case Integer n -> "reprovado com " + n;
case String s -> "nota em texto? \"" + s + "\"";
default -> "não é nota";
};
}
public static void main(String[] args) {
System.out.println(classificar(9));
System.out.println(classificar(6));
System.out.println(classificar(3));
System.out.println(classificar("dez"));
System.out.println(classificar(8.5));
}
}
$ javac Main.java && java Main aprovado com 9 recuperação com 6 reprovado com 3 nota em texto? "dez" não é nota
- case
- Começa uma alternativa do
switch. - Integer n
- O padrão de tipo: casa com inteiros e cria
n. - when n >= 7
- A guarda: só vale se, além de ser inteiro,
nfor pelo menos 7. Ela pode usar a variável do padrão. - -> "aprovado com " + n
- O resultado deste caso.
O 6 não passou na guarda do primeiro case, então o Java tentou o segundo, que casou. O 8.5 é um Double, não casou com nenhum padrão de Integer nem de String, e caiu no default.
O caso mais específico vem primeiro. case Integer n sem guarda casa com qualquer inteiro; se ele vier antes de case Integer n when n >= 7, o segundo nunca seria alcançado, e o javac recusa o programa (veja "Quando dá errado", mais abaixo).
Missão: a etiqueta de cada item da loja depende do tipo e do tamanho. Complete o switch com padrões e when: textos de até 4 letras viram fruta curta: …, os outros fruta longa: …; inteiros acima de 10 viram caixa grande com …, os outros caixa pequena com ….
public class Main {
static String etiqueta(Object item) {
return switch (item) {
case String s when s.length() <= 4 -> "fruta curta: " + s;
// falta: os outros textos e os inteiros
default -> "não sei";
};
}
public static void main(String[] args) {
System.out.println(etiqueta("kiwi"));
System.out.println(etiqueta("melancia"));
System.out.println(etiqueta(24));
System.out.println(etiqueta(6));
}
}
public class Main {
static String etiqueta(Object item) {
return switch (item) {
case String s when s.length() <= 4 -> "fruta curta: " + s;
case String s -> "fruta longa: " + s;
case Integer n when n > 10 -> "caixa grande com " + n;
case Integer n -> "caixa pequena com " + n;
default -> "não sei";
};
}
public static void main(String[] args) {
System.out.println(etiqueta("kiwi"));
System.out.println(etiqueta("melancia"));
System.out.println(etiqueta(24));
System.out.println(etiqueta(6));
}
}
4. E o null? case null Java 21+
null quer dizer "nenhum objeto". Ele não é String, nem Integer, nem nada; por isso null instanceof String dá false. Mas um switch que recebe null e não diz o que fazer com ele lança uma exceção, mesmo tendo default:
public class Main {
static String descrever(Object item) {
return switch (item) {
case Integer n -> "inteiro " + n;
case String s -> "texto " + s;
default -> "outra coisa";
};
}
public static void main(String[] args) {
System.out.println(descrever("caqui"));
System.out.println(descrever(null));
}
}
$ javac Main.java && java Main texto caqui Exception in thread "main" java.lang.NullPointerException at java.base/java.util.Objects.requireNonNull(Objects.java:220) at Main.descrever(Main.java:3) at Main.main(Main.java:12)
A exceção NullPointerException ("exceção de ponteiro nulo") apareceu na linha 3, a do switch. O default não pega o null: isso foi decidido para que programas antigos continuassem se comportando igual. Para tratar o null, escreva um caso para ele:
return switch (item) {
case null -> "nada (null)";
case Integer n -> "inteiro " + n;
case String s -> "texto " + s;
default -> "outra coisa";
};
$ javac Main.java && java Main texto caqui nada (null)
Se o null deve ter o mesmo destino do default, junte os dois: case null, default -> "nulo ou outra coisa";.
5. Record patterns: desmontar um record Java 21+
Um record é uma classe feita só para guardar dados, como record Ponto(int x, int y). Os record patterns (padrões de record) deixam você testar "é um Ponto?" e, ao mesmo tempo, tirar o x e o y de dentro dele. Isso se chama desconstrução: o contrário de construir com new Ponto(3, 4).
record Ponto(int x, int y) {}
public class Main {
public static void main(String[] args) {
Object o = new Ponto(3, 4);
// Antes: testar, converter e chamar os métodos de acesso
if (o instanceof Ponto p) {
System.out.println("x = " + p.x() + ", y = " + p.y());
}
// Java 21+: desmontar o record direto no padrão
if (o instanceof Ponto(int x, int y)) {
System.out.println("x = " + x + ", y = " + y);
}
// var também vale dentro do padrão
if (o instanceof Ponto(var x, var y) && x == y) {
System.out.println("Está na diagonal");
} else {
System.out.println("Fora da diagonal");
}
}
}
$ javac Main.java && java Main x = 3, y = 4 x = 3, y = 4 Fora da diagonal
- o instanceof
- O teste de sempre.
- Ponto(
- "É um
Ponto? Então abra-o." - int x, int y
- Um padrão para cada componente, na ordem em que o record os declara. Os nomes são seus:
Ponto(int a, int b)também funcionaria. - )
- Fecha o padrão. Se casar,
xeyjá existem com os valores de dentro.
No lugar dos tipos, você pode escrever var e deixar o Java descobrir (int, neste caso), como na aula de var.
Padrões aninhados: um record dentro do outro
A força dos record patterns aparece quando um record guarda outro. Uma Linha tem dois Pontos; num único padrão, você abre a linha e os dois pontos:
record Ponto(int x, int y) {}
record Linha(Ponto inicio, Ponto fim) {}
public class Main {
static String descrever(Object o) {
return switch (o) {
case Ponto(var x, var y) when x == 0 && y == 0 -> "a origem";
case Ponto(var x, var y) -> "ponto (" + x + ", " + y + ")";
case Linha(Ponto(var x1, var y1), Ponto(var x2, var y2)) when x1 == x2 ->
"linha vertical em x = " + x1;
case Linha(Ponto a, Ponto b) -> "linha de " + a + " até " + b;
default -> "não sei";
};
}
public static void main(String[] args) {
System.out.println(descrever(new Ponto(0, 0)));
System.out.println(descrever(new Ponto(2, 5)));
System.out.println(descrever(new Linha(new Ponto(1, 0), new Ponto(1, 9))));
System.out.println(descrever(new Linha(new Ponto(0, 0), new Ponto(4, 4))));
}
}
$ javac Main.java && java Main a origem ponto (2, 5) linha vertical em x = 1 linha de Ponto[x=0, y=0] até Ponto[x=4, y=4]
O terceiro case lê assim: "uma Linha cujo início é um Ponto com x1 e y1, e cujo fim é um Ponto com x2 e y2, quando x1 == x2". No último case, os pontos não foram abertos: a e b são Pontos inteiros, e o println mostra o toString automático dos records (Ponto[x=0, y=0]).
_ Java 22+
Se o y não interessa, escreva _ (sublinhado) no lugar dele: o instanceof Ponto(int x, _). O mesmo vale para o nome inteiro: case Ponto _ -> "é um ponto". São as unnamed variables and patterns, finais desde o Java 22 (veja Novidades do Java 17 ao 25). No JDK 25 da Ana, if (o instanceof Ponto(int x, _)) mostrou Só me interessa o x: 3.
6. Exaustividade: o switch precisa cobrir tudo
Um switch que produz um valor precisa ter uma resposta para toda entrada possível. Os case juntos devem ser exaustivos ("esgotar todas as possibilidades"). Com Object, as possibilidades são infinitas (qualquer classe!), então o default é obrigatório. Sem ele:
public class Main {
static String descrever(Object item) {
return switch (item) {
case Integer n -> "inteiro " + n;
case String s -> "texto " + s;
};
}
public static void main(String[] args) {
System.out.println(descrever(10));
}
}
$ javac Main.java
Main.java:3: error: the switch expression does not cover all possible input values
return switch (item) {
^
1 error
"A expressão switch não cobre todos os valores de entrada possíveis." E se o tipo tiver um número fechado de possibilidades? É para isso que existem as classes seladas: uma interface sealed lista quem pode implementá-la, e o compilador passa a saber que não existe mais nenhuma. Aí o default se torna desnecessário:
sealed interface Forma permits Circulo, Retangulo {}
record Circulo(double raio) implements Forma {}
record Retangulo(double largura, double altura) implements Forma {}
public class Main {
static double area(Forma f) {
return switch (f) {
case Circulo(double r) -> Math.PI * r * r;
case Retangulo(double l, double a) -> l * a;
};
}
public static void main(String[] args) {
System.out.println(area(new Circulo(1)));
System.out.println(area(new Retangulo(3, 4)));
}
}
$ javac Main.java && java Main 3.141592653589793 12.0
A vantagem aparece quando alguém aumenta a família. Se você acrescentar Triangulo ao permits e esquecer de atualizar o switch, o programa não compila, e o javac aponta o lugar exato:
$ javac Main.java
Main.java:8: error: the switch expression does not cover all possible input values
return switch (f) {
^
1 error
Com um default, o triângulo cairia nele em silêncio e talvez calculasse uma área errada. Sem o default, o compilador vira seu aliado. A aula Classes Seladas detalha sealed, permits, final e non-sealed.
Antes × depois
Resumo de como o mesmo problema era escrito antes e como se escreve no Java moderno:
| Situação | Antes | Depois | Desde |
|---|---|---|---|
| Testar o tipo e usar | if (o instanceof String) { String s = (String) o; … } | if (o instanceof String s) { … } | Java 16 |
| Tipo + condição | if (o instanceof String) { String s = (String) o; if (s.length() > 3) … } | if (o instanceof String s && s.length() > 3) | Java 16 |
| Vários tipos | Escada de if … else if com casts | switch (o) { case Integer n -> …; case String s -> …; } | Java 21 |
| Tipo + condição no switch | if dentro do case | case Integer n when n >= 7 -> … | Java 21 |
| Ler os dados de um record | Ponto p = (Ponto) o; int x = p.x(); int y = p.y(); | if (o instanceof Ponto(int x, int y)) | Java 21 |
null no switch | if (o == null) … antes do switch | case null -> … | Java 21 |
| Garantir que todos os tipos foram tratados | default -> throw … e torcer | Interface sealed + switch sem default | Java 17/21 |
| Ignorar uma parte | Dar um nome que nunca é usado | Ponto(int x, _) | Java 22 |
Qual usar?
| Você quer… | Use |
|---|---|
Testar um tipo, num if | instanceof com padrão |
| Escolher entre vários tipos | switch com padrões |
| Separar casos pelo valor, além do tipo | case … when condição |
| Usar os componentes de um record | Record pattern: Ponto(var x, var y) |
| Que o compilador avise quando surgir um tipo novo | Interface sealed e switch sem default |
| Rodar num Java 11 ou 8 | O jeito antigo (instanceof + cast); padrões não existem lá |
Quando dá errado
Usar a variável de padrão fora do if
public class Main {
public static void main(String[] args) {
Object coisa = "uva";
if (coisa instanceof String s) {
System.out.println("É texto");
}
System.out.println(s.length());
}
}
$ javac Main.java
Main.java:7: error: cannot find symbol
System.out.println(s.length());
^
symbol: variable s
location: class Main
1 error
O que quer dizer: "não encontrei o símbolo s". Na linha 7 o Java não tem certeza de que coisa é String (o if pode ter dado false), então s não existe ali. Como consertar: use s só dentro do if, ou inverta o teste com if (!(coisa instanceof String s)) return;, como na boasVindas.
Usar a variável depois de um ||
if (coisa instanceof String s || s.isEmpty()) {
$ javac Main.java
Main.java:4: error: cannot find symbol
if (coisa instanceof String s || s.isEmpty()) {
^
symbol: variable s
location: class Main
1 error
O que quer dizer: o lado direito do || só roda quando o esquerdo é false, isto é, quando coisa não é String; aí não há s. Como consertar: provavelmente você queria &&: coisa instanceof String s && s.isEmpty().
Caso geral antes do caso específico
return switch (nota) {
case Integer n -> "reprovado com " + n;
case Integer n when n >= 7 -> "aprovado com " + n;
default -> "não é nota";
};
$ javac Main.java
Main.java:5: error: this case label is dominated by a preceding case label
case Integer n when n >= 7 -> "aprovado com " + n;
^
1 error
O que quer dizer: "este rótulo de case é dominado por um rótulo anterior". case Integer n já pega todo inteiro, então o caso de baixo nunca seria usado. Como consertar: do mais específico para o mais geral: primeiro os casos com when, depois o case Integer n sozinho.
Switch sem default e sem cobrir tudo
Mensagem: error: the switch expression does not cover all possible input values (veja a seção 6). Como consertar: acrescente o case que falta (se o tipo for sealed) ou um default.
NullPointerException no switch
O valor era null e não havia case null (seção 4). Como consertar: case null -> … ou case null, default -> ….
Referência
| Forma | Exemplo | Significado |
|---|---|---|
| Padrão de tipo | o instanceof String s | Se o é String, cria s |
| Padrão + condição | o instanceof String s && !s.isEmpty() | s vale à direita do && |
| Negação + saída | if (!(o instanceof String s)) return; | s vale depois do if |
| Switch com tipos | case Integer n -> … | Escolhe pelo tipo |
| Guarda | case Integer n when n > 0 -> … | Tipo e condição |
| Nulo | case null -> … / case null, default -> … | Trata null sem exceção |
| Record pattern | case Ponto(int x, int y) -> … | Testa e desmonta |
Com var | Ponto(var x, var y) | Tipos inferidos |
| Aninhado | Linha(Ponto(var a, var b), Ponto p) | Abre records dentro de records |
| Ignorar | Ponto(int x, _), case Ponto _ | Parte sem nome (Java 22+) |
Exercícios
1. Complete para testar se o é um String e chamá-lo de nome:
2. Complete a guarda para que o caso valha só para inteiros negativos:
3. Complete o caso que trata o valor nulo:
4. Em if (o instanceof String s || s.isEmpty()), o que acontece?
5. Por que um switch sobre uma interface sealed pode dispensar o default?
6. Um switch com default, mas sem case null, recebe null. O que acontece?
Missão final: a loja da Ana aceita dois tipos de pagamento, e a interface é sealed. Escreva o switch sem default, com record patterns: Pix(String chave, double valor) vira Pix de 50.0 para ana@email.com; um Cartao com 1 parcela vira Cartão à vista: 80.0; com mais parcelas, Cartão em 4x de 30.0 (valor dividido pelas parcelas).
sealed interface Pagamento permits Pix, Cartao {}
record Pix(String chave, double valor) implements Pagamento {}
record Cartao(int parcelas, double valor) implements Pagamento {}
public class Main {
static String descrever(Pagamento p) {
return switch (p) {
// escreva os casos aqui
};
}
public static void main(String[] args) {
System.out.println(descrever(new Pix("ana@email.com", 50.0)));
System.out.println(descrever(new Cartao(1, 80.0)));
System.out.println(descrever(new Cartao(4, 120.0)));
}
}
sealed interface Pagamento permits Pix, Cartao {}
record Pix(String chave, double valor) implements Pagamento {}
record Cartao(int parcelas, double valor) implements Pagamento {}
public class Main {
static String descrever(Pagamento p) {
return switch (p) {
case Pix(String chave, double valor) -> "Pix de " + valor + " para " + chave;
case Cartao(int parcelas, double valor) when parcelas == 1 -> "Cartão à vista: " + valor;
case Cartao(int parcelas, double valor) -> "Cartão em " + parcelas + "x de " + (valor / parcelas);
};
}
public static void main(String[] args) {
System.out.println(descrever(new Pix("ana@email.com", 50.0)));
System.out.println(descrever(new Cartao(1, 80.0)));
System.out.println(descrever(new Cartao(4, 120.0)));
}
}
Resumo
- Um padrão testa a forma de um objeto e já extrai as partes dele, sem cast.
o instanceof String sJava 16+ criasonde o teste é certamente verdadeiro: dentro doif, à direita do&&ou depois de umif (!…) return;. Fora disso:cannot find symbol.- O
switchaceita padrões de tipo e guardaswhenJava 21+; os casos são testados de cima para baixo, do mais específico ao mais geral. - Record patterns Java 21+ desmontam records, inclusive aninhados;
vare_ajudam a escrever menos. - Um switch que produz valor precisa ser exaustivo; com uma interface
sealed, dá para dispensar odefaulte deixar o compilador avisar quando surgir um tipo novo. nullnum switch semcase nulllançaNullPointerException, mesmo comdefault.
Próxima aula: Classes Seladas, a outra metade desta história.