Correspondência de Padrões em Java
Use correspondência de padrões em Java para instanceof e switch — padrões de tipo, padrões de record e desconstrução.
Durante anos, o código Java que trabalhava com valores de tipo desconhecido seguia um ritual tedioso: testar o tipo com instanceof, depois converter para esse tipo e então utilizá-lo. A correspondência de padrões colapsa esse ritual em uma única expressão. Um padrão descreve a forma dos dados; se um valor corresponde, o Java vincula suas partes a variáveis que podem ser usadas imediatamente — sem conversão manual.
A correspondência de padrões chegou em etapas: primeiro os padrões de instanceof, depois padrões em switch, e então os padrões de record que desconstroem records em seus componentes. Juntos, permitem escrever código declarativo e seguro em relação a tipos, que se lê como os dados com que opera.
Este capítulo cobre o padrão de instanceof, padrões de tipo em switch, padrões com guarda e tratamento de null, e padrões de record — depois os une em um programa executável. Ele se baseia em três recursos que convém revisar antes: o operador instanceof, records e expressões switch.
Correspondência de Padrões para instanceof
O padrão clássico de testar e converter precisava de três referências ao mesmo tipo. O padrão de instanceof vincula uma variável ao mesmo tempo que realiza o teste, e o vínculo está em escopo em qualquer lugar em que o teste é comprovadamente verdadeiro.
Object value = "hello";
// Old way: test, then cast
if (value instanceof String) {
String s = (String) value;
System.out.println(s.length());
}
// Pattern way: test and bind together
if (value instanceof String s) {
System.out.println(s.length());
}Como a variável de vínculo participa da expressão booleana, você pode continuar restringindo no mesmo if. O compilador prova que s é seguro de usar:
if (value instanceof String s && s.length() > 3) {
System.out.println(s.toUpperCase());
}Padrões em switch
Um switch pode corresponder a padrões de tipo, despachando pelo tipo em tempo de execução do seletor. Cada case vincula o valor correspondido, de modo que o corpo trabalha diretamente com uma variável tipada. Isso transforma longas cadeias de if/else instanceof em uma tabela compacta e legível.
static String format(Object value) {
return switch (value) {
case Integer i -> "int: " + i;
case Long l -> "long: " + l;
case String s -> "string: " + s;
default -> "other: " + value;
};
}Um switch com padrão de tipo deve ser exaustivo — precisa cobrir todas as entradas possíveis. Para seletores Object arbitrários, isso significa um ramo default; para hierarquias sealed, o compilador conhece o conjunto completo de subtipos e pode verificar a exaustividade sem um default.
Padrões com Guarda e null
Uma cláusula when adiciona uma condição booleana a um case, permitindo que dois valores do mesmo tipo tomem ramos diferentes. Isso é chamado de padrão com guarda, e a ordem importa: cases com guarda mais específicos vêm antes do fallback sem guarda.
static String size(String s) {
return switch (s) {
case String t when t.isEmpty() -> "empty";
case String t when t.length() < 5 -> "short";
case String t -> "long (" + t.length() + ")";
};
}Tradicionalmente, um switch lançava NullPointerException com um seletor null. Um switch com padrão pode tratar null explicitamente com um case null, mantendo a verificação de nulo dentro do mesmo construto em vez de uma guarda separada antes dele.
| Recurso | Sintaxe | Propósito |
|---|---|---|
| Padrão de tipo | case String s | Corresponder por tipo e vincular |
| Padrão com guarda | case String s when s.isEmpty() | Adicionar uma condição a um case |
| Rótulo null | case null | Corresponder a um seletor null |
| Padrão de record | case Point(int x, int y) | Desconstruir um record |
Padrões de Record
Um padrão de record corresponde a um record e vincula seus componentes em uma só operação, eliminando as chamadas de acessor. Como os records expõem seus componentes, o compilador conhece a forma exata e permite nomear cada parte inline. Padrões de record são aninhados, portanto é possível desestruturar um record de records.
record Point(int x, int y) {}
record Line(Point start, Point end) {}
static String render(Object o) {
return switch (o) {
case Point(int x, int y) -> "point " + x + "," + y;
// Nested: pull both endpoints' coordinates out at once
case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
"line " + x1 + "," + y1 + " -> " + x2 + "," + y2;
default -> "unknown";
};
}A correspondência de padrões brilha com tipos sealed: quando uma interface lista suas implementações permitidas, um switch sobre elas é exaustivo sem um default, e adicionar um novo subtipo transforma o case ausente em um erro de compilação em vez de um bug silencioso.
Um Exemplo Completo e Executável
O programa abaixo une as peças. Ele usa um padrão de instanceof com guarda, uma hierarquia Shape sealed de records, padrões de record que desconstroem cada forma em um switch, um padrão com guarda que identifica um quadrado, e um case null — tudo sem um único cast explícito.
O que observar na execução:
describe(42)imprimepositive int 42porque a guardainstanceof Integer i && i > 0testa o tipo e o valor juntos antes de vinculari.describe(-5)cai emunknown— o mesmo padrãoIntegercorresponde ao tipo, mas a guardai > 0falha, mostrando como uma guarda refina um padrão de tipo.- O switch de
areanão precisa dedefault:Shapeé sealed, portanto listarCircle,RectangleeTriangleé exaustivo e o compilador fica satisfeito. - O retângulo
5.0 x 5.0é impresso comosquare side=5.0porque seu case com guardawhen w == hestá posicionado antes do case geralRectangle re tem precedência. - A linha final imprime
no shape: o ramocase nulltrata um seletornulldentro do switch em vez de lançarNullPointerException.