DEV Community

Cover image for Evolução do Switch no Java
Wesley Egberto
Wesley Egberto

Posted on

Evolução do Switch no Java

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");
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 um yield 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
};
Enter fullscreen mode Exit fullscreen mode
  • 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";
};
Enter fullscreen mode Exit fullscreen mode

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");
};
Enter fullscreen mode Exit fullscreen mode

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 -> "???"
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode
  • 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);
Enter fullscreen mode Exit fullscreen mode

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)