sábado, 20 de julho de 2019

Novo switch no Java 12

A JDK 12 foi liberada em março de 2019. Ela foi a terceira release dentro do ciclo semestral de liberação de release anunciado junto ao lançamento do Java 9. Esse artigo é focado no novo recurso da linguagem disponível em modo preview: o novo switch.

Novo switch

Para os entusiastas da linguagem que adoram quando algum recurso é adicionado, irão gostar do Java 12 que introduz um switch com diversas melhorias em relação ao anterior. Entretanto é importante ressaltar que no momento esse recurso só pode ser usado no modo preview. Isso significa que se estiver fazendo uso da nova sintaxe do switch e tentar compilar do modo usual, não irá funcionar. Para habilitar esse feature é necessário usar as flags --enable-preview --release 12 no momento da compilação. Caso queira compilar e executar os códigos de exemplo desse artigo deve se certificar que tem a JDK 12 instalada. Além dos códigos presentes no corpo do artigo, foi disponibilizado nesse repositório do Github uma classe de exemplo que ilustra todas as melhorias que serão abordadas até o final do texto. Portanto se assim desejar pode clonar o repo e utilizar o comando abaixo para compilar a classe Java:


javac --enable-preview --release 12 SwitchTest.java

Após a compilação execute o arquivo .class gerado usando o comando abaixo(é necessário usar a flag --enable-preview):


java --enable-preview SwitchTest

Antes de olharmos em detalhes o novo switch, precisamos entender o que seria o modo preview. Um feature disponibilizado em modo preview, foi plenamente especificado e implementado, porém não é necessariamente permanente. Tal feature é incluso na JDK para que os desenvolvedores possam dar feedback baseados em situações reais de uso, o que pode influenciar para que o feature se torne permanente posteriormente. Portanto, o recurso ainda pode ser melhorado ou até removido.

Dito isso, vamos ao que interessa. O que há de errado com o switch atual que conhecemos? Veremos quatro melhorias que o novo switch trás em relação ao atual: problema do fall-through, forma composta, exaustão e formato de expressão.

Problema do fall-through

Vamos começar com o comportamento fall-through. Em Java, geralmente escrevemos um switch da seguinte forma:

switch(userStatus) {
 case PENDING:
  //faz algo
  break;
 case ACTIVE:
  //faz algo
  break;
 default:
  //faz algo
  break;
}

Note que cada bloco case tem um break correspondente. A declaração de um break assegura que o próximo bloco case dentro do switch não seja executado. O que acontece se você esquecer a declaração de cada break? O código continuará compilando normalmente? Sim, ele irá compilar. Então, tente adivinhar a saída no console referente ao código abaixo:


var userStatus = UserStatus.PENDING;
 
switch(userStatus) {
 case PENDING:
  System.out.println("Pending user");
 case ACTIVE:
  System.out.println("Active user");
 default:
  System.out.println("Unknown user status");
}

Esse código imprime:

Pending user
Active user
Unknown user status

Esse comportamento do switch se chama fall-through. A documentação oficial Oracle referente ao switch do Java explica: "All statements after the matching case label are executed in sequence, regardless of the expression of subsequent case labels, until a break statement is encountered.". Expressando o mesmo conceito em português, podemos dizer que todas as instruções que vem após o case correpondente - aquele que corresponde ao que está dentro dos parenteses do switch - são executadas em sequência, independente se existem outras labels case, até que seja encontrada uma instrução break.

O fall-through pode levar a bugs sutis quando o programador simplesmente esquece de colocar uma instrução break. Consequentemente o comportamento do programa pode ficar incorreto. Inclusive o compilador Java contempla uma opção -Xint:fallthrough que alerta a respeito de algum fall through suspeito. Esse erro também é detectado por code checkers, como o Error Prone.

Esse problema é mencionado também na JDK Enhancement Proposal(JEP) 325 como uma motivação para a forma aprimorada do switch. Parafraseando em português o que está escrito na JEP 325, é dito que o atual design do switch do Java segue de perto linguagens como C e C++, e suporta o fall through por padrão. Dando continuidade a paráfrase, ainda é mencionado que embora esse controle de fluxo tradicional seja frequentemente útil para escrever código de baixo nível, no contexto de alto nível sua natureza propensa a erros começa a superar sua flexibilidade.

Agora no Java 12(com --enable-preview habilitado), existe uma nova sintaxe para o switch que não tem fall through, e justamente por isso ajuda a reduzir a possibilidade de bugs. Abaixo apresentamos como refatorar o código anterior fazendo uso do novo switch:


switch(userStatus) {
 case PENDING -> System.out.println("Pending user");
 case ACTIVE -> System.out.println("Active user");
 default -> System.out.println("Unknown user status");
};

Esse novo switch tem uma sintaxe ao estilo lambda, introduzida no Java 8. Note que não são de fato expressões lambda, mas apenas usam uma sintaxe lambda-like. É possível usar expressões de uma única linha ou blocos delimitados por chaves exatamente da mesma maneira que fazemos com o corpo das expressões lambda. Aqui está um exemplo que concilia expressões de uma única linha com um bloco delimitado por chaves:


switch(userStatus) {
 case PENDING -> {
  System.out.println("Pending user");
  counter++;
 }
 case ACTIVE -> System.out.println("Active user");
 default -> System.out.println("Unknown user status");
};

Forma composta

Essa melhoria permite um case com múltiplas labels. Antes do Java 12 era permitido apenas uma label para cada case. Por exemplo, no código abaixo, apesar da lógica dos status PENDING e ACTIVE ser a mesma, seria preciso lidar com isso em cases separados a menos que usássemos o fall through:


switch(userStatus) {
 case DELETED:
  System.out.println("Deleted user");
  break;
 case PENDING:
  System.out.println("Not deleted user");
  break;
 case ACTIVE:
  System.out.println("Not deleted user");
  break;
}

Uma maneira típica de diminuir a verbosidade é usar a sintaxe fall-through do switch dessa forma:


switch(userStatus) {
 case DELETED:
  System.out.println("Deleted user");
  break;
 case PENDING:
 case ACTIVE:
  System.out.println("Not deleted user");
  break;
}

No entanto, como discutido anteriormente, esse estilo pode ocasionar bugs já que não fica claro se a ausência do break foi proposital ou intencional. Se houvesse uma forma de especificar que a lógica é a mesma para PENDING e ACTIVE, isso tornaria o código mais claro. Essa é exatamente uma das melhorias que o Java 12 traz. Usando a sintaxe de flecha você pode especificar múltiplas labels para um case. Podemos refatorar o código anterior para que fique assim:

switch (userStatus) {
 case DELETED ->
  System.out.println("Deleted user");
 case PENDING, ACTIVE -> //case composto por duas labels
  System.out.println("Not deleted user");
}

Nesse código as labels são listadas de forma consecutiva. Dessa forma o código se tornou mais conciso e a intenção do programador se tornou evidente.

Exaustão

Um outro beneficio no novo switch é a exaustão. Isso significa que quando se utiliza um switch com enum, o compilador verifica se para cada valor possível do enum existe um case correspondente. Em outras palavras todas as opções do enum são exploradas de forma exaustiva dentro da estrutura do switch.

Por exemplo, se você tivesse o seguinte enum:

public enum UserStatus{
 PENDING, ACTIVE, INACTIVE, DELETED
}

Se você criar um switch que cobre algumas possibilidades dentro do enum mas não todos seus valores, como o seguinte:

var status = switch(userStatus) {
 case PENDING -> "Pending";
 case ACTIVE -> "Active";
 case INACTIVE -> "Inactive";
 //faltou o case para DELETED
};

System.out.println(status);

Então, no Java 12, o compilador irá gerar esse erro:

error: the switch expression does not cover all possible input values

Esse erro é um lembrete útil que alerta a ausência de uma cláusula default ou que faltam cases para lidar com todos possíveis valores do enum. Ressalto que fiz testes com as versões 12 e 12.0.2 da JDK, e essa checagem só ocorreu quando utilizei o formato de expressão, uma outra melhoria do switch que veremos a seguir(repare que o switch do último exemplo de código retorna um valor que é atribuído a variável status).

Formato de expressão

O formato de expressão é uma outra melhoria em relação ao velho switch. Para esclarecer o que "formato de expressão" significa é válido revisarmos diferença entre uma instrução e uma expressão.

Instruções são em essência "ações". No entanto, expressões são "requisições" que produzem um valor/resultado. Expressões são fundamentais e simples de entender, o que nos leva a escrevermos código mais compreensível e de fácil manutenção.

Em Java podemos ver claramente a distinção entre um uma instrução if e um operador ternário que é uma expressão. O código a seguir ilustra essa diferença:


String message = "";
if(condition) {
 message = "Hello!";
}else {
 message = "Welcome!";
}

Esse código poderia ser reescrito em formato de expressão assim:

String message = condition ? "Hello!" : "Welcome!";

Antes do Java 12 a sintaxe do switch admitia apenas o formato de instrução. Entretanto agora você pode fazer uso do formato de expressão para escrever seu switch. Por exemplo, veja esse código que processa vários status de usuário:

String message = "Unknown status";
switch(userStatus) {
 case PENDING:
  message = "Pending user";
  break;
 case ACTIVE:
  message = "Active user";
  break;
}

O código acima pode ser reescrito de forma mais concisa usando a sintaxe de expressão do novo switch que deixa mais evidente a intenção do código:

var message = switch(userStatus) {
 case PENDING -> "Pending user";
 case ACTIVE -> "Active user";
 default -> "Unknown status";
};

Note que agora esse switch retorna um valor - é uma expressão.

Conclusão

O Java 12 não traz nenhum feature novo que pode ser facilmente usado. Entretanto, traz o novo switch, um feature disponível no modo preview. O novo switch é uma adição muito útil que provê ao desenvolvedor uma maneira de escrever código mais conciso e menos propenso a erros. Especificamente, o novo switch traz consigo quatro melhorias: sintaxe que evita o fall-through acidental, forma composta, exaustão e formato de expressão.

Fonte: esse texto é praticamente uma tradução parcial do artigo "New switch Expressions in Java 12" que saiu na Java Magazine, edição Maio/Junho 2019, com exceção de algumas adições de observações minhas e diferenças de códigos pois os adaptei para deixá-los mais próximos de exemplos da vida real.

Nenhum comentário:

Postar um comentário