Do statement até expression e pattern matching.
O que cobriremos nesse post:
- Switch statement
- Switch expression (Java 14+)
- Pattern matching (type pattern e record pattern)
Antes de iniciar, gostaria de fazer a seguinte distinção entre statement e expression:
- Statement (declaração): qualquer código que é executado como uma únidade. Esse código pode executar um método, declarar algo (declaration statement), pode mudar o fluxo do programa (control flow statement), pode atribuir um valor (assignment statement), criar um objeto (object creation expression), criar uma lambda (lambda expression).
- Expression (expressão): código que quando executado resultado em um único valor (expression statement).
No Java, os statements são executados de uma maneira top-bottom, conforme aparecem no seu programa.
Os statements de controle de fluxo são utilizado para esse comportamento ao efetuar a mudança da execução através de diferentes caminho no código.
Um dos recursos disponibilizado pelo Java para fazer controle de fluxo é o switch
.
Um switch statement permite você executar diferentes caminhos no código de acordo com um teste lógico que é executado.
Diferente de um if-else statement, aqui podemos testar uma variável contra diferentes valores de uma vez e de forma mais clara.
Switch Statement
Até o Java 11, um switch statement era utilizado somente para controle de fluxo.
Um exemplo para entender a estrutura geral de um bloco switch:
String classe = "Mago";
switch (classe) { // 1
case "Mago": // 2
System.out.println("Gandalf"); // 3
break; // 3
case "Arqueiro": // 2
System.out.println("Legolas");
break;
case "Guerreiro":
System.out.println("Aragorn");
break;
default: // 4
System.out.println("Classe inválida");
}
No código acima temos:
- 1: variável ou constante que será utilizada para testar os labels do bloco switch.
- 2: switch label, utiliza um valor para testar contra a variável, se for verdadeiro o seu bloco de código será executado.
- 3: block de código do case para execução, pode ou não ser terminada por um
break
. - 4: break é utilizado para finalizar a execução do bloco do switch, caso não seja utilizado ocorrera um fall through.
- 5: case padrão que será executado caso todos os outros testes lógicos falhem.
A variável ou constante que será utilizada no switch precisa ser um dos seguintes tipos:
- tipo primitivo:
byte
,short
,char
,int
; - tipo referência: tipo enumerado (enum),
String
ou um tipo wrapper (Character
,Byte
,Short
,Integer
).
Fall Through
Caso o bloco de código do case em execução não tenha um break
, os blocos de código dos próximos case serão executados (como se fossem verdadeiro) até que se encontre um break ou o bloco switch termine.
Exemplo:
int nota = 8;
switch (nota) {
case 1:
case 2:
case 3:
case 4:
case 5:
System.out.println("Tá péssimo, precisa melhorar!");
break;
case 6:
case 7:
System.out.println("Está bacana, mas há espaço para evolução!");
break;
case 8:
case 9:
System.out.println("Está excelente!");
break;
case 10:
System.out.println("Está perfeito!");
break;
default:
System.out.println("Nota inválida");
}
Acima, as notas entre 1 e 5 executarão o mesmo bloco de código.
Observe que os cases de 1 a 4 não tem um bloco de código explícito, é assumido um bloco vazio e apenas ocorre o fall through.
Switch Expression (Java 14+)
Até o Java 11, switch podia ser utilizado apenas como statement de controle de fluxo.
Se quiséssemos usar um switch para atribuir uma variável, tínhamos duas opções:
// atribuir uma varável local em cada case
String nota = "B";
String mensagem;
switch (nota) {
case "A":
mensagem = "Está perfeito!";
break;
case "B":
mensagem = "Está muito bem!";
break;
case "C":
mensagem = "Está razoável, podemos melhorar!";
break;
case "D":
mensagem = "Está mal, vamos melhorar!";
break;
case "E":
mensagem = "Está péssimo, bora pra cima!";
break;
case "F":
mensagem = "Desisto!";
break;
default:
mensagem = "Nota inválida!";
}
// ou usar um método para deixar mais claro e sucinto
public String getMensagemParaNota(String nota) {
switch (nota) {
case "A":
return "Está perfeito!";
case "B":
return "Está muito bem!";
case "C":
return "Está razoável, podemos melhorar!";
case "D":
return "Está mal, vamos melhorar!";
case "E":
return "Está péssimo, bora pra cima!";
case "F":
return "Desisto!";
default:
return "Nota inválida!";
}
}
// e usar
String mensagem = getMensagemParaNota("B");
Para os dois casos, é trabalhoso e neboluso devido à verbosidade do código.
Mas a partir do Java 12, foi proposta uma JEP para alterar o comportamento do switch para que possamos utilizá-lo como uma expression.
No Java 12 (JEP 325) e 13 (JEP 354), essa nova feature foi disponibilizada como preview para que a comunidade pudesse testar e dar o feedback.
E a partir do Java 14, a JEP 361 tornou Switch Expression uma feature padrão na linguagem.
Essa JEP introduziu as seguintes mudanças no switch:
- 1. Um block switch pode resultar em um valor, que pode ser utilizado em um assignment expression (ex.:
String mensagem = switch (nota) { ... }
). - 2. Introduziu uma nova forma de switch label que utiliza uma arrow label (ex.:
case 0 -> "Zero"
). - 3. Passou a requerer exhaustiveness de cases em um Switch Expression (cobertura de todos os possíveis valores).
Switch Expression
Agora com Switch Expression, podemos atribuir variáveis ou retornar valores diretamente do switch.
Para isso, foi introduzido um statement com uma palavra-chave contextual yield
para indicar o retorno de um valor do switch.
E não podemos misturar os blocos do switch label com break
e yield
pois, respectivamente, break
é utilizado
somente em switch statement e yield
é utilizado semente em switch expression.
Seguindo nosso exemplo anterior, podemos reescrever o switch:
String nota = "B";
String mensagem = switch (nota) { // 1
case "A":
yield "Está perfeito!"; // 2
case "B":
yield "Está muito bem!";
case "C":
yield "Está razoável, podemos melhorar!";
case "D":
yield "Está mal, vamos melhorar!";
case "E":
yield "Está péssimo, bora pra cima!";
case "F":
yield "Desisto!";
default:
System.err.println("Nota inválida: " + nota); // 3
yield "Nota inválida!";
}; // 4
Algumas notas sobre o switch expression anterior:
- 1: atribuímos diretamente a variável com o switch, também poderíamos usar
return
ou passar um argumento para uma chamada de método. - 2: usamos o
yield
para retornar o valor, caso não tenha umyield
também ocorrerá o fall through para o próximo switch label. - 3: podemos ter outros códigos no bloco da mesma forma.
- 4: precisa terminar com ponto-e-vírgula.
O uso do switch expression com yield
é bacana nos casos que precisamos executar algumas operações antes de retornar o valor de fato.
Mas na maioria dos casos iremos retornar apenas um valor ou uma expression (ex.: resultado de um outro método), para simplificar ainda mais foi introduzido o arrow label.
O formato de switch label com case L:
é chamado oficialmente de switch labeled statement group.
Tipo do Retorno
Importante destacar que um switch expression tem várias expressions de retorno, podemos ter diferentes tipos.
Arrow Labels
Um arrow label é formado por um switch label e uma arrow expression: case "A" ->
.
Se o teste do switch label for verdadeiro, apenas a expression ou statement à direita da arrow que será executada, não ocorrerá o fall through como no caso devido
tradicional case "A" :
.
A expression ou statement à direita tem a mesma construção de um lambda expression: um statement/expression de uma linha ou um bloco com chaves.
String nota = "B";
String mensagem = switch (nota) {
case "A" -> "Está perfeito!"; // 1
case "B" -> "Está muito bem!";
case "C" -> "Está razoável, podemos melhorar!";
case "D" -> "Está mal, vamos melhorar!";
case "E" -> "Está péssimo, bora pra cima!";
case "F" -> "Desisto!";
default -> { // 2
System.err.println("Nota inválida: " + nota);
yield "Nota inválida!"; // 3
} // 4
};
- 1: com arrow label podemos só por o valor de retorno ou chamada de método (nunca ocorrerá o fall through) e precisa terminar com ponto-e-vírgula.
- 2: caso precisemos executar várias instruções, podemos utilizar um bloco de código e retornar no final com
yield
. - 3: com bloco de código sempre precisamos terminar com
yield
ou lançar exception, já que não ocorre o fall through. - 4: o bloco de código não precisa terminar com ponto-e-vírgula.
Caso precisemos que múltiplos switch labels retornem o mesmo valor, precisamos combinar todos eles em um case
só:
int nota = 8;
String mensagem = switch (nota) {
// aqui todos os labels resultam no mesmo valor
case 1, 2, 3, 4, 5 -> "Tá péssimo, precisa melhorar!";
case 6, 7 -> "Está bacana, mas há espaço para evolução!";
case 8, 9 -> "Está excelente!";
case 10 -> "Está perfeito!";
default -> "Nota inválida";
};
Também podemos usar o arrow label em um switch statement normalmente:
int nota = 8;
switch (nota) {
case 1, 2, 3, 4, 5 -> {
System.out.println("Tá péssimo, precisa melhorar!");
if (nota > 4) {
break; // ainda podemos utilizar para sair mais cedo do block
}
System.out.println("Agendar reunião com os pais...");
}
case 6, 7 -> System.out.println("Está bacana, mas há espaço para evolução!");
case 8, 9 -> System.out.println("Está excelente!");
case 10 -> System.out.println("Está perfeito!");
default -> System.out.println("Nota inválida");
};
Exhaustiveness
Para utilizar um switch expression, precisamos fazer a exaustão dos possíveis valores do tipo da variável ou valor que estamos utilizado no switch.
Isso significa que precisamos ter um switch label para cada valor possível ou ter um default
(na maioria dos casos onde usarmos os tipos primitivos ou uma String
).
Caso o switch expression não trate todos os possíveis valores ocorrerá um erro de compilação (isso é legal porque nos ajuda a identificar facilmente os pontos de alteração por exemplo quando utilizamos um switch com enum em vários lugares).
enum Classe { GUERREIRO, MAGO, ARQUEIRO, BARDO };
Classe classe = Classe.MAGO;
String nome = switch (classe) {
case GUERREIRO -> "Aragorn";
case MAGO -> "Gandalf";
case ARQUEIRO -> "Legolas";
case BARDO -> "???";
// caso falte algum valor para cobrir, precisamos usar o default
// default -> "???"
}
Pattern Matching para Switch (Java 17+)
Da mesma forma que Switch Expression iniciou-se como uma feature em preview até ser incorporada por completa no Java.
Pattern Matching também foi introduzida como preview através de diferentes releases.
No Java 17, começou como preview e se manteve com esse status até o Java 20 (JEPs 406, 420, 427 e 433).
No Java 21, a JEP 441 lançou Pattern Matching for switch como padrão na linguagem.
Pattern Matching
Para entendermos o poder switch precisamos antes entender o que é pattern matching.
Pattern matching é uma poderosa técnica que existe desde a década de 1960 que é implementada em diversas linguagens de programação.
Um pattern matching nos permite fazer verificação do tipo e/ou da estrutura do dado de uma forma mais declarativa e orientada à dados.
O Java 17 suporta dois tipos de pattern matching:
- Type pattern: onde verificamos o tipo da variável/valor; lançado no Java 16.
- Record pattern: onde, além de aplicar type pattern, também podemos desconstruir um record para obter seus componentes; lançado no Java 21.
Pattern Matching com instanceof
Começaremos utilizando o operador instanceof
para entender pattern matching.
A JEP 394 tornou a feature para suportar pattern matching no operador instanceof
padrão no Java 16.
Utilizamos o operador instanceof
para verificar se determinada variável é de um determinado tipo, e normalmente logo em seguida fazemos o cast explícito para esse tipo.
Pattern matching pode ajudar nesse cenário pois além de fazer o matching do tipo também é possível capturar o valor em uma nova variável já com o tipo desejado.
Ex.:
Object valor = "Texto recebido de algum lugar";
// Até o Java 16: forma de fazer a verificação do tipo até
if (valor instanceof String) {
String texto = (String) value;
System.out.printlnt("Tamanho do texto: " + texto.length());
}
// A partir do Java 16: podemos usar pattern matching para capturar a variável no tipo desejado
if (valor instanceof String texto) {
System.out.printlnt("Tamanho do texto: " + texto.length());
}
Observe que com pattern matching foi possível testar o tipo da variável valor
e já criar uma nova variável fazendo o cast implícito para
o tipo.
E a variável local declarada no pattern tem o escopo somente do if
.
Um type pattern é composto por dois componentes:
- predicado (ou teste): que é utilizado para aplicar na variável alvo, no exemplo temos
instanceof String
. - variáveis pattern: variável utilizada para capturar o valor caso o predicado seja verdadeiro, no exemplo temos
String texto
.
O predicado do pattern matching é bem poderoso, podemos estender as condições utilizando a variável do pattern, que nesse momento já sabemos o tipo e capturamos em uma nova variável após o instanceof
.
Mas sempre lembrando que precisamos utilizar o E lógico (&&
) pois a nossa garantia está em cima do predicado do instanceof
.
Ex.:
Object objeto = "Curto";
// podemos verificar junto a String capturada
if (objeto instanceof String texto && texto.length() < 10) {
System.out.println("Texto curto");
}
Também podemos usar outros tipos quaisquer no type pattern, como record:
record Ponto(int x, int y) {}
Object objeto = new Ponto(10, 20);
if (objeto instanceof Ponto p && p.x() > 0) {
int x = p.x();
int y = p.y();
System.out.println("Coordenadas do ponto: " + x + "," + y);
}
E para melhorar ainda mais, a partir do Java 21, a JEP 440 tornou padrão a feature de record pattern onde podemos aplicar pattern matching nos records e extrair seus componentes.
O exemplo anterior onde utilizamos apenas type pattern pode ser melhorado ainda mais ao utiliza record pattern:
record Ponto(int x, int y) {}
Object objeto = new Ponto(10, 20);
if (objeto instanceof Ponto(int x, var y) && x > 0) {
System.out.println("Coordenadas do ponto: " + x + "," + y);
}
Com record pattern, podemos obter os componentes do record declarando variáveis seguindo sua estrutra.
No exemplo declaramos int x
e var y
(que será um inteiro).
Pattern Matching com switch
Agora voltando ao switch
, tudo que vimos do uso de pattern matching podemos utilizar nos switch labels!
Graças a JEP 441 que tornou a feature padrão no Java 21! 🚀
Além disso, o switch também foi alterado para receber qualquer tipo, não somente aqueles básicos limitados. Isso torna os nossos switches muitos mais poderosos e menos verbosos.
E tem mais! Agora também podemos usar o null
como switch label (null case), eliminando a necessidade de ficar fazendo verificação da variável antes de usar no switch.
Abaixo segue um exemplo completo onde utilizamos um switch com type pattern:
Object objeto = "Valor desconhecido";
String mensagem = switch (objeto) {
case Integer i -> String.format("int %d", i); // 1
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
case null -> String.format("NULL"); // 2
case int[] arr -> String.format("Array %s", arr);
default -> objeto.toString(); // 3
};
System.out.println(mensagem);
Note que:
- 1: podemos aplicar type pattern da mesma forma para capturar o tipo.
- 2: podemos testar se é um valor nulo, simplificado e deixando explícito o tratamento, sem isso poderia ocorre um NPE.
- 3: por causa da exhaustiveness, precisamos de um case default para capturar todos os outros tipos existentes.
Guarded Case Label
No switch também podemos estender as condições da mesma forma com o if
, mas aqui pode ser um refinamento do case que estamos deixando mais explícito.
Esse case label é chamado de guarded case label e a condição que estamos estendendo é chamado de guard.
Um exemplo para ilustrar:
Object resposta = "Sim";
String mensagem = switch (resposta) {
case null -> "Ops, informe sua resposta!";
case String s -> {
if (s.equalsIgnoreCase("SIM"))
yield "Vamos jogar!";
if (s.equalsIgnoreCase("NAO"))
yield "Que pena, volte amanhã!";
yield "Não entendi, informe Sim/Nao";
}
default -> "Opção inválida, informe Sim/Nao";
};
System.out.println(mensagem);
O código acima, utilizando guarded case label fica mais simples e sucinto:
Object resposta = "Sim";
String mensagem = switch (resposta) {
case null -> "Ops, informe sua resposta!";
case String s when s.equalsIgnoreCase("SIM") -> "Vamos jogar!";
case String s when s.equalsIgnoreCase("NAO") -> "Que pena, volte amanhã!";
default -> "Opção inválida, informe Sim/Nao";
};
System.out.println(mensagem);
Utilizamos a palavra-chave contextual when
para indicar a guard do nosso case.
Dominância
E por último, um ponto importante sobre os cases do switch é que devemos respeitar a hierarquia de tipos pois um case pode ter dominância sobre outro ao ser um super tipo dele.
É a mesma coisa que acontece com a captura de exceções em um try-catch
com muitos catch
.
Exemplo:
Number valor = 10L;
String mensagem = switch (valor) {
case null -> "NULL";
case Byte b -> String.format("byte %b", b);
// ao
// case Number n -> String.format("Number %s", n); // 1
// Erro: this case label is dominated by a preceding case label
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case Number n -> String.format("Number %s", n); // 2
};
- 1: deixar o
Number
fará com que esse case demine todos os próximos cases porque é um supertipo de todos ele. - 2: precisamos deixar os tipos mais genéricos da árvore no final, note também que não precisamos de um default já que temos o tipo mais genérico possível.
Com tudo isso em mente, também podemos misturar type pattern com constantes, mas sempre respeitando os casos de dominância.
Integer valor = 42;
String mensagem = switch (valor) {
case null -> "NULL";
case 0 -> "Zerado";
// aqui tratamos o 42 especial para não cair no próximo case
case 42 -> "Resposta do Universo";
case Integer i when i > 0 -> "Valor positivo";
case Integer i -> "Valor negativo";
};
System.out.println(mensagem);
Exhaustiveness (Java 17+)
Com a JEP 441, o compilador somente vai requerer exhaustiveness para os seguintes casos (por motivo de compatibilidade):
- se o switch utiliza qualquer pattern;
- se existe um
null
label; - se a expressão de seleção não é um dos tipos "legados" (
char
,byte
,short
,int
,Character
,Byte
,Short
,Integer
,String
ou um enum.
Conclusão
Toda essas novas features trazidas na linguagem e incorporada no switch
está tornando a linguagem cada vez mais poderosa, menos verbosa e mais simples de se utilizar e entender.
A tendência é que novas formas de pattern matching entrem na linguagem (como desconstrução de qualquer classe).
Com isso terminamos a nossa longa jornada através da evolução do switch
na linguagem Java.
Neste repositório do Github tenho alguns exemplos diferentes de switch com pattern matching onde busquei explorar as novas capacidades trazidas pela JEP.
Tenho também diversas outros exemplos mostrando outras features que foram lançadas no Java.
Top comments (0)