sexta-feira, 15 de novembro de 2019

Apostila de Java Gratuita

Pessoal, esse post é para divulgar uma apostila gratuita que estou desenvolvendo para servir de suporte aos meus alunos de Java e fortalecer a comunidade Java. Link da apostila gratuita de Java no meu GitHub.

Parafraseando uma vendedora que certa vez ouvi: "Aproveite pois é grátis, totalmente gratuito, sem pagar nada" kkkkkkk.

Podem deixar críticas e sugestões nos comentários.

Abraço a todos.

terça-feira, 27 de agosto de 2019

Ordenação de listas no Java 8

Nesse artigo iremos abordar as possíveis formas de ordenar listas no Java 8.

Ordenando lista de objetos que não implementam Comparable

Para começarmos a trabalhar com ordenação, primeiramente vamos criar uma lista de produtos:

Product product1 = new Product(4, "Notebook Samsung", new BigDecimal("1200.00"));
Product product2 = new Product(3, "Notebook Dell", new BigDecimal("1200.00"));
Product product3 = new Product(2, "TV", new BigDecimal("3100.00"));
Product product4 = new Product(1, "Sofá", new BigDecimal("2500.00"));

List<Product> products = Arrays.asList(product1, product2, product3, product4);

Acima temos uma lista de produtos, e a classe Product não implementa Comparable, portanto precisamos criar uma implementação de Comparator para fazer a ordenação. Antes do Java 8 podíamos passar para o método Collections.sort a lista a ser ordenada juntamente com a implementação de Comparator:

Comparator<Product> comparator = new Comparator<>() {
 @Override
 public int compare(Product p1, Product p2) {
  return p1.getName().compareTo(p2.getName());
 }
};

Collections.sort(products, comparator);

Uma das variações do código acima, mas agora fazendo uso do Java 8, é utilizar uma expressão lambda no lugar de classe anônima para gerar uma implementação de Comparator, já que ela é uma interface funcional:

Comparator<Product> comparator = (p1, p2) -> p1.getName().compareTo(p2.getName());
Collections.sort(products, comparator);

Simplificando em uma única linha temos:

Collections.sort(products, (p1, p2) -> p1.getName().compareTo(p2.getName()));

Na versão 8 do Java também foi incluído um método default chamado sort na interface List. Podemos utilizá-lo no lugar do Collections.sort, basta invocá-lo diretamente a partir da nossa lista de produtos:

products.sort((p1, p2) -> p1.getName().compareTo(p2.getName()));

As novidades nessa versão não param por ai, ainda foram adicionados alguns métodos estáticos na interface Comparator que funcionam como uma factory de Comparators. Por exemplo podemos utilizar o Comparator.comparing que recebe uma Function e devolve um Comparator:

Function<Product, String> extractName = p -> p.getName();
products.sort(Comparator.comparing(extractName));

O código acima foi quebrado em duas linhas para fins didáticos e de clareza de código, mas podemos simplificá-lo em uma única linha da seguinte forma:

products.sort(Comparator.comparing(p -> p.getName()));

Visando um código mais fácil de entender que favoreça a manutenibilidade, podemos passar para o comparing um method reference(outro recurso introduzido na versão 8 da linguagem) no lugar de uma expressão lambda:

products.sort(Comparator.comparing(Product::getName));

A respeito do method reference, pode ser usado no lugar de uma expressão lambda quando a mesma não faz nada além de chamar um único método existente, como nesse caso que chama apenas p.getName(). Nesses casos geralmente é sempre mais claro chamar o método existente pelo nome através de um method reference. Podemos dizer que os method references são um tipo especial de expressão lambda que são mais fáceis de ler.

Ordenando listas de objetos que implementam Comparable

Abaixo temos uma lista de Strings. Como String implementa Comparable podemos ordenar uma lista de Strings facilmente usando Collections.sort, o que não é nenhuma novidade pois era a forma usada até a versão anterior do Java:

List<String> series = Arrays.asList("Friends", "Prison Break", "Breaking Bad", "Black Mirror");
Collections.sort(series);

O código acima produz a seguinte saída:

[Black Mirror, Breaking Bad, Friends, Prison Break]

Podemos reescrever o código acima utilizando o método default chamado sort que se encontra presente em List na versão 8 do Java, porém é necessário que passemos um Comparator. Um Comparator para ordenar em ordem natural pode ser conseguido através de Comparator.naturalOrder():

series.sort(Comparator.naturalOrder());

O código acima produz a seguinte saída:

[Black Mirror, Breaking Bad, Friends, Prison Break]

Para ordenar na ordem inversa usamos Comparator.reverseOrder():

series.sort(Comparator.reverseOrder());

O código acima produz a seguinte saída:

[Prison Break, Friends, Breaking Bad, Black Mirror]

Evitando autoboxing desnecessário no sort

No código abaixo a ordenação é feita pelo id do produto que é do tipo primitivo int. Dividimos o código em múltiplas linhas com fins didáticos para melhor entendimento. Tivemos que criar uma Function que contém um método apply que irá receber um Product e retornar um Integer ao invés de int. Por conta disso irá ocorrer autoboxing toda vez que esse método for invocado, e ele pode ser invocado muitas vezes durante o sort. Veja:

Function<Product, Integer> extractId = p -> p.getId();
Comparator<Product> comparator = Comparator.comparing(extractId);
products.sort(comparator);

Para evitar autoboxing desnecessário podemos utilizar ToIntFunction ao invés de Function e também fazer uso de Comparator.comparingInt no lugar de Comparator.comparing:

ToIntFunction<Product> extractId = p -> p.getId();
Comparator<Product> comparator = Comparator.comparingInt(extractId);
products.sort(comparator);

O objetivo do artigo era dar um panorama geral das possibilidades mais comuns de ordenação de listas usando Java 8, mais do que ser uma referência extensiva. Espero que tenham gostado.

Para quem quiser executar os códigos do artigo, estão disponíveis no GitHub.

Pô-pô-por hoje é só pe-pe-pessoal! (os mais velhos pegaram a referência a Looney Tunes hahaha)

quarta-feira, 21 de agosto de 2019

Java 8 - Expressões Lambda e Interfaces funcionais

A linguagem Java surgiu em 1995, portanto já completou 24 anos de existência. Levando em conta sua história, podemos destacar o lançamento do Java 5 em 2004 que foi um marco para a linguagem pois foram incluídas mudanças significativas, das quais podemos ressaltar enums, anotações e generics. Em 2014, com a chegada do Java 8 pudemos observar uma nova onda de mudanças.

Apesar do Java 8 ter sido lançado a alguns anos, tenho observado no mercado que muitas empresas ainda não adotaram essa versão ainda ou estão em processo de migração. Por conta disso escreverei uma série de artigos sobre os recursos incluídos no Java 8 para agregar valor a comunidade e ajudar os desenvolvedores que ainda não se atualizaram ou que precisam de referências para consultar caso estejam começando a utilizar essa versão profissionalmente.

Esse primeiro artigo dessa série irá abordar as expressões lambda e as interfaces funcionais - talvez essas tenham sido as novidades mais comentadas na época do lançamento. Caso você queira executar os códigos desse post, tudo está disponível nesse repositório do GitHub.

Antes do Java 8(usando Anonymous Inner Classes)

Uma forma didática de explicar esses novos conceitos é comparando-os com os códigos equivalentes da versão anterior. Vamos então ver como fazíamos antes para iniciar uma Thread que usasse uma implementação de Runnable. Nesse caso a primeira coisa seria criar uma implementação de Runnable dessa forma:

public class RunnableImplementationExample implements Runnable {

 @Override
 public void run() {
  System.out.println("Running usual implementation...");
 }

}

Agora que criamos uma classe chamada RunnableImplementationExample, que implementa Runnable, podemos instanciá-la e passá-la como argumento para o construtor de Thread. Depois basta iniciar a Thread com o método start(). Veja:


Runnable runnable = new RunnableImplementationExample();
Thread thread = new Thread(runnable);
thread.start();

Outra forma muito utilizada para fazer a mesma coisa é fazer uso de uma classe anônima conforme o código a seguir:

Runnable runnable = new Runnable() {
 public void run() {
  System.out.println("Running anonymous 1...");
 }
};

Thread thread = new Thread(runnable);
thread.start();

Apesar dessa sintaxe ser um pouco estranha pois usa new em conjunto com o nome da interface(que nesse caso é Runnable), não estamos criando uma instância da interface, mas sim uma instância de uma classe anônima(sem nome) que implementa a interface.

Geralmente quando utilizamos uma classe anônima, é usada de forma pontual uma única vez no código, portanto não é necessário nessas ocasiões fazer a atribuição a uma variável. Podemos passar a instância da classe anônima direto no construtor da Thread desse jeito:

Thread thread = new Thread(new Runnable() {
 public void run() {
  System.out.println("Running anonymous 2...");
 }
});
thread.start();

Note que essa maneira é muito verborrágica, ou seja, temos que escrever uma quantidade excessiva de código para tarefas triviais. No entanto não havia o que fazer para contornar isso até o Java 7, mas ai que as expressões lambda do Java 8 entram em cena.

Expressões Lambda

Uma expressão lambda pode ser definida como uma maneira mais simples e menos verbosa de implementar uma interface que tem um único método abstrato(uma interface do tipo SAM que significa "Single Abastract Method" ou simplesmente uma interface funcional). Em outras palavras podemos dizer que através de uma expressão lambda podemos criar um código equivalente a uma classe anônima. Vejamos o mesmo código reescrito com Java 8 fazendo uso de uma expressão lambda:


Runnable runnable = () -> {System.out.println("Running lambda 1...");};
Thread thread = new Thread(runnable);
thread.start();

Usando uma expressão lambda conseguimos diminuir consideravelmente o montante de código final. Como estamos atribuindo o resultado da expressão lambda para uma variável do tipo Runnable, e essa interface tem apenas um método, tudo funciona perfeitamente como em um passe de mágica e nem precisamos deixar explícito qual método da interface está sendo sobrescrito(o nome do método não aparece, apenas um par de parênteses sem nada dentro, seguido do símbolo -> e do corpo da expressão enclausurado entre chaves).

Podemos simplificar ainda mais esse código:

Thread thread = new Thread(() -> System.out.println("Running lambda 2..."));
thread.start();

Passamos a expressão lambda direto ao construtor de Thread, além de removermos as chaves { } do bloco e o ponto e vírgula - o que só é possível devido ao bloco ter apenas uma instrução.

Os códigos apresentados até essa parte do artigo se encontram no pacote br.com.bruno.lambdaexamples.example1 no projeto que se encontra no GitHub.

Interfaces funcionais

Se abrirmos o código fonte da interface Runnable perceberemos a anotação @FunctionalInterface:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface Runnable is used
     * to create a thread, starting the thread causes the object's
     * run method to be called in that separately executing
     * thread.
     * * The general contract of the method run is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

Essa annotation indica que essa interface é uma interface funcional, ou seja, ela deve ter apenas um único método abstrato - nesse caso possui apenas o método run(). Uma interface funcional pode até ter outros métodos desde que eles sejam default(outra novidade do Java 8 que veremos brevemente mais adiante) ou estáticos.

Como você já deve ter percebido as expressões lambda trabalham em conjunto com as interfaces funcionais e só funcionam com esse tipo de interface. A anotação @FunctionalInterface é opcional, mas é uma boa prática para assegurar que sua interface sempre terá apenas um método abstrato e que ninguém irá adicionar outro método abstrato por engano. Caso alguém tente adicionar mais um método abstrato em uma interface que possui essa annotation gerará erro de compilação:

NomeDaInterface.java:3: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  NomeDaInterface is not a functional interface
    multiple non-overriding abstract methods found in interface NomeDaInterface
1 error

Apresentado o conceito de interface funcional, vamos a um exemplo prático onde criaremos uma interface com a anotação @FunctionalInterface e faremos uso dela em conjunto com uma expressão lambda. Segue definição da nossa interface:

@FunctionalInterface
public interface ValidatorInterfaceExample {
 boolean validate(T t);
}

Nesse exemplo criamos uma interface com o objetivo de fazer validações. Agora vamos criar uma implementação dessa interface, por meio de uma classe anônima, para validar CEPs:


ValidatorInterfaceExample<String> zipCodeValidator = new ValidatorInterfaceExample<>() {

 @Override
 public boolean validate(String zipCode) {
  return zipCode.matches("[0-9]{5}-[0-9]{3}");
 }
 
};

System.out.println(zipCodeValidator.validate("06455-906"));

Esse código imprime true.

Reescrevendo esse código com expressão lambda ficaria assim:

ValidatorInterfaceExample<String> zipCodeValidator = 
  (String zipCode) -> {return zipCode.matches("[0-9]{5}-[0-9]{3}");};
System.out.println(zipCodeValidator.validate("06455-906"));

Podemos simplificar ainda mais esse código retirando o tipo do argumento, pois o compilador consegue inferir o tipo automaticamente. Como o bloco é composto de apenas uma linha também é possível se livrar das chaves que o envolvem, além de retirar a palavra return e o ponto e vírgula:


ValidatorInterfaceExample<String> zipCodeValidator = 
  zipCode -> zipCode.matches("[0-9]{5}-[0-9]{3}");
System.out.println(zipCodeValidator.validate("06455-906"));

Os códigos dessa parte do artigo se encontram no pacote br.com.bruno.lambdaexamples.example2 no projeto que está no GitHub.

Interfaces funcionais predefinidas

Além de algumas interfaces já conhecidas de versões anteriores como o Runnable, Readable, Iterable, Comparable, entre outras, se enquadrarem como interfaces funcionais e serem reconhecidas como tais pela JVM, e além de podermos criar nossas próprias interfaces funcionais, no pacote java.util.function introduzido no Java 8 encontramos uma variedade de interfaces funcionais predefinidas(Function, BiFunction, UnaryOperator, BiOperator, Predicate, Supplier, Consumer, dentre outras). Segue uma relação de algumas das principais interfaces funcionais com suas respectivas descrições:

Interface Funcional Descrição
Function Define uma função que recebe um parâmetro e retorna um resultado. O tipo do resultado pode ser diferente do tipo do parâmetro.
BiFunction Define uma função que recebe dois parâmetros e retorna um resultado. O tipo do resultado pode ser diferente do tipo de qualquer parâmetro.
UnaryOperator Representa uma operação em um único operando que retorna um resultado cujo tipo é o mesmo do operando. A interface UnaryOperator pode ser entendida como se fosse uma Function que retorna um valor que é do mesmo tipo do parâmetro. De fato, UnaryOperator é uma subinterface de Function(UnaryOperator extends Function).
BiOperator Representa uma operação com dois operandos. O resultado e os operandos devem ser do mesmo tipo.
Predicate Uma Function que recebe um parâmetro e retorna true ou false baseado no valor do parâmetro.
Supplier Representa um fornecedor de resultados.
Consumer Uma operação recebe um parâmetro porém não retorna nenhum resultado.

Para tornar o artigo mais prático, vamos a um exemplo utilizando a interface funcional Function, que modela uma função que recebe um parâmetro e retorna um resultado. O tipo do resultado pode ser diferente do tipo do parâmetro. O código desse exemplo irá converter milhas em quilômetro:

package br.com.bruno.lambdaexamples.example3;

import java.util.function.Function;

public class FunctionExample {

 public static void main(String[] args) {
  int miles = 3;
  double kilometers = convertMilesToKilometers(miles);
  System.out.printf("%d miles = %3.2f kilometers\n", miles, kilometers);
 }

 private static double convertMilesToKilometers(int miles) {
  /*
   *  Utilizando a interface Function
   *  que é usada para criar uma função que recebe um argumento e retorna um valor.
   *  Nesse exemplo recebe um Integer e retorna um Double.
   *  Unboxing e Autoboxing ocorrem sem problemas nesse código.
   */
  Function<Integer, Double> milesToKilometers = (m) -> 1.6 * m;
  return milesToKilometers.apply(miles);
 }

}

Esse exemplo usando Function está no GitHub no pacote br.com.bruno.lambdaexamples.example3.

Conforme dito anteriormente uma interface funcional podem conter apenas um método abstrato, entretanto não existe problema algum se ela também contiver outros métodos desde que eles sejam default ou static. Esse cenário se aplica a interface funcional Function que possui um único método abstrato chamado apply, um método static chamado identity e outros dois métodos default chamados compose e andThen. Vejamos um exemplo utilizando todos os métodos dessa interface. A primeira parte do exemplo faz uso dos métodos compose e andThen:

Function<Integer, Integer> multiply = (value) -> value * 2;
Function<Integer, Integer> add = (value) -> value + 3;

Function<Integer, Integer> addThenMultiply = multiply.compose(add);

System.out.println(addThenMultiply.apply(3)); //Imprime 12

Function<Integer, Integer> multiplyThenAdd = multiply.andThen(add);

System.out.println(multiplyThenAdd.apply(3)); //Imprime 9

Basicamente tanto um método como o outro retornam uma função composta. O que varia é a ordem de execução das funções que originaram a função composta quando é chamado o apply. Quando a composição é feita dessa forma multiply.compose(add) a função add executa primeiro e o resultado é passado para a função multiply que é executada na sequência. Já se estruturarmos a composição desse jeito multiply.andThen(add) é executada primeiro a função multiply e o resultado passado para a função add que executa em seguida.

Mas ainda resta a pergunta: onde esses dois métodos estão implementados? Dentro da interface? Isso mesmo, a implementação deles está dentro da interface Function. A partir do Java 8 podemos ter métodos com implementação dentro das interfaces, eles são os métodos default(na documentação do beta também podem ser referenciados como defender methods ou extension methods). Vamos entender um pouco melhor a motivação para criação desse recurso. Imagine se na interface List a Oracle colocasse mais um método abstrato, o que aconteceria? Todas as implementações de List teriam que implementar esse método. Isso geraria um trabalho enorme, mas esse não é o principal problema. Ao atualizar para o Java 8, as bibliotecas que tem sua própria implementação de List iriam quebrar, como é o caso do Hibernate. Por isso a decisão final foi optar pelos métodos default que possibilitam que coloquemos uma implementação de um método na interface e as classes que implementam tal interface passam a ter esse método também, de certa forma as implementações "herdam" esse método.

Por último vamos a segunda parte desse exemplo final que mostra como utilizar o método identity():

Function<Integer,Integer> theSame = Function.identity();
System.out.println(theSame.apply(5)); //Imprime 5, pois essa Function retorna o próprio argumento que recebeu

Basicamente esse método serve para retornar uma função que sempre retorna o mesmo argumento que recebeu.

Está disponível no GitHub esse exemplo mostrando como usar cada método da interface Function no pacote br.com.bruno.lambdaexamples.example4 dentro do projeto.

Com isso chegamos ao final desse texto. Em artigos posteriores serão abordados outros assuntos relativos as outras novidades e recursos interessantes do Java 8. Caso você tenha gostado desse texto ou tenha sugestões para próximos artigos comente no post.

Até a próxima javeiros!!!

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.