Guia completo para alternar expressões em Java 12


O bom e velho switch está em Java desde o primeiro dia. Todos nós o usamos e estamos acostumados - especialmente suas peculiaridades. (Alguém mais se incomoda com a break ?) Mas agora tudo começa a mudar: no Java 12, o switch em vez de um operador se tornou uma expressão:


 boolean result = switch(ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); // as we'll see in "Exhaustiveness", `default` is not necessary default -> throw new IllegalArgumentException("Seriously?!"); }; 

O switch agora tem a capacidade de retornar o resultado de seu trabalho, que pode ser atribuído a uma variável; Você também pode usar a sintaxe no estilo lambda, que permite livrar-se da passagem para todos os case em que não há declaração de break .


Neste guia, mostrarei tudo o que você precisa saber sobre expressões de chave no Java 12.


Pré-visualização


De acordo com a especificação preliminar da linguagem , as expressões de switch estão apenas começando a ser implementadas no Java 12.


Isso significa que essa construção de controle pode ser alterada em versões futuras da especificação de idioma.


Para começar a usar a nova versão do switch você precisa usar a opção de linha de comando --enable-preview durante a compilação e durante a inicialização do programa (você também deve usar --release 12 ao compilar - observação do tradutor).


Portanto, lembre-se de que o switch , como expressão, atualmente não possui a sintaxe final no Java 12.


Se você deseja brincar com tudo isso sozinho, pode visitar meu projeto de demonstração do Java X em um github .


Problema com declarações no switch


Antes de avançarmos para uma visão geral das inovações no switch , vamos avaliar rapidamente uma situação. Suponha que nos deparemos com um boulean ternário "terrível" e deseje convertê-lo em um boulean regular. Aqui está uma maneira de fazer isso:


 boolean result; switch(ternaryBool) { case TRUE: result = true; // don't forget to `break` or you're screwed! break; case FALSE: result = false; break; case FILE_NOT_FOUND: // intermediate variable for demo purposes; // wait for it... var ex = new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); throw ex; default: // ... here we go: // can't declare another variable with the same name var ex2 = new IllegalArgumentException("Seriously?!"); throw ex2; } 

Concorde que isso é muito inconveniente. Como muitas outras opções de opção encontradas em "nature", o exemplo acima simplesmente calcula o valor de uma variável e a atribui, mas a implementação é ignorada (declare o result do identificador e use-o mais tarde), repetida (meu break 'e sempre o resultado de copiar massa) e propenso a erros (esqueceu outro ramo? Oh!). Há claramente algo a melhorar.


Vamos tentar resolver esses problemas colocando o switch em um método separado:


 private static boolean toBoolean(Bool ternaryBool) { switch(ternaryBool) { case TRUE: return true; case FALSE: return false; case FILE_NOT_FOUND: throw new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); // without default branch, the method wouldn't compile default: throw new IllegalArgumentException("Seriously?!"); } } 

Isso é muito melhor: não há variável fictícia, não há break desordenando o código e as mensagens do compilador sobre a ausência do default (mesmo que isso não seja necessário, como neste caso).


Mas, se você pensar bem, não somos obrigados a criar métodos apenas para contornar o recurso de linguagem estranha. E isso é mesmo sem considerar que essa refatoração nem sempre é possível. Não, precisamos de uma solução melhor!


Apresentando expressões de switch!


Como mostrei no início do artigo, começando com Java 12 e superior, você pode resolver o problema acima da seguinte maneira:


 boolean result = switch(ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); // as we'll see in "Exhaustiveness", `default` is not necessary default -> throw new IllegalArgumentException("Seriously?!"); }; 

Eu acho que isso é bastante óbvio: se ternartBool for TRUE , o result 'será definido como true (em outras palavras, TRUE se tornará true ). FALSE se torna false .


Dois pensamentos surgem imediatamente:


  • switch pode ter um resultado;
  • o que há com as flechas?

Antes de me aprofundar nos detalhes dos novos recursos do switch , no começo vou falar sobre esses dois aspectos principais.


Expressão ou declaração


Você pode se surpreender que a opção agora seja uma expressão. Mas o que ele era antes?


Antes do Java 12, um switch era um operador - uma construção imperativa que regula o fluxo de controle.


Pense nas diferenças entre as versões antiga e nova do switch como a diferença entre if e o operador ternário. Ambos verificam a condição lógica e executam ramificações, dependendo do resultado.


A diferença é que, if apenas executar o bloco correspondente, enquanto o operador ternário retornar algum resultado:


 if(condition) { result = doThis(); } else { result = doThat(); } result = condition ? doThis() : doThat(); 

O mesmo vale para o switch : antes do Java 12, se você quisesse calcular o valor e salvar o resultado, você o designaria a uma variável (e depois break ) ou o retornaria de um método criado especificamente para a switch .


Agora, toda a expressão da instrução switch é avaliada (a ramificação correspondente é selecionada para execução) e o resultado dos cálculos pode ser atribuído a uma variável.


Outra diferença entre a expressão e a instrução é que a instrução switch , por fazer parte da instrução, deve terminar com ponto-e-vírgula, diferentemente da instrução switch clássica.


Seta ou dois pontos


O exemplo introdutório usou a nova sintaxe no estilo lambda com uma seta entre o rótulo e a parte em execução. É importante entender que, para isso, não é necessário usar o switch como uma expressão. De fato, o exemplo abaixo é equivalente ao código fornecido no início do artigo:


 boolean result = switch(ternaryBool) { case TRUE: break true; case FALSE: break false; case FILE_NOT_FOUND: throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); default: throw new IllegalArgumentException("Seriously?!!?"); }; 

Note que agora você pode usar break com um valor! Isso se encaixa perfeitamente com switch estilo antigo que usam break sem qualquer significado. Então, nesse caso, uma seta significa uma expressão em vez de um operador, por que está aqui? Apenas sintaxe hipster?


Historicamente, as marcas de dois pontos simplesmente marcam o ponto de entrada no bloco de instruções. A partir deste ponto, a execução de todo o código abaixo começa, mesmo quando outro rótulo é encontrado. No switch sabemos disso como passando para o próximo case (fall-through): o rótulo do case determina para onde o fluxo de controle salta. Para completá-lo, você precisa break ou return .


Por sua vez, usar a seta significa que apenas o bloco à direita será executado. E não "falha".


Mais sobre a evolução do switch


Várias tags no caso


Até agora, cada case apenas um rótulo. Mas agora tudo mudou - um case pode corresponder a vários rótulos:


 String result = switch(ternaryBool) { case TRUE, FALSE -> "sane"; // `default, case FILE_NOT_FOUND -> ...` does not work // (neither does other way around), but that makes // sense because using only `default` suffices default -> "insane"; }; 

O comportamento deve ser óbvio: TRUE e FALSE produzem o mesmo resultado - a expressão "sane" é avaliada.


Essa é uma inovação bastante agradável que substituiu o uso múltiplo de case quando foi necessário implementar uma transição de passagem para o próximo case .


Tipos fora do enum


Todos os exemplos de enum neste artigo usam enum . E os outros tipos? Expressões e switch também podem funcionar com String , int , (verifique a documentação ) short , byte , char e seus wrappers. Nada mudou aqui até agora, embora a idéia de usar tipos de dados como float e long ainda seja válida (do segundo para o último parágrafo).


Mais sobre a flecha


Vejamos duas propriedades específicas para a forma de seta de um registro separador:


  • falta de transição de ponta a ponta para o próximo case ;
  • blocos de operadores.

Sem passagem para o próximo caso


Aqui está o que o JEP 325 diz sobre isso:


O design atual da switch em Java está intimamente relacionado a linguagens como C e C ++ e, por padrão, suporta semântica de ponta a ponta. Embora esse método de controle tradicional seja frequentemente útil para escrever código de baixo nível (como analisadores para codificação binária), como o switch usado no código de nível superior, os erros dessa abordagem começam a superar sua flexibilidade.

Concordo totalmente e saúdo a oportunidade de usar o switch sem comportamento padrão:


 switch(ternaryBool) { case TRUE, FALSE -> System.out.println("Bool was sane"); // in colon-form, if `ternaryBool` is `TRUE` or `FALSE`, // we would see both messages; in arrow-form, only one // branch is executed default -> System.out.println("Bool was insane"); } 

É importante saber que isso não tem nada a ver com o uso de switch como expressão ou declaração. O fator decisivo aqui é a flecha contra o cólon.


Blocos do Operador


Como no caso de lambdas, a seta pode apontar para um operador (como acima) ou um bloco destacado com chaves:


 boolean result = switch(Bool.random()) { case TRUE -> { System.out.println("Bool true"); // return with `break`, not `return` break true; } case FALSE -> { System.out.println("Bool false"); break false; } case FILE_NOT_FOUND -> { var ex = new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); throw ex; } default -> { var ex = new IllegalArgumentException("Seriously?!"); throw ex; } }; 

Os blocos que precisam ser criados para operadores de várias linhas têm uma vantagem adicional (que não é necessária ao usar dois pontos), o que significa que, para usar os mesmos nomes de variáveis ​​em ramificações diferentes, o switch não requer processamento especial.


Se lhe pareceu incomum sair de blocos usando break vez de return , não se preocupe - isso também me intrigou e pareceu estranho. Mas então eu pensei sobre isso e cheguei à conclusão de que faz sentido, pois preserva o estilo antigo da construção de switch , que usa break sem valores.


Saiba mais sobre as instruções do switch


E por último, mas não menos importante, as especificidades do uso de switch como uma expressão:


  • expressões múltiplas;
  • retorno antecipado ( return antecipado);
  • cobertura de todos os valores.

Observe que não importa qual formulário é usado!


Várias expressões


Expressões de comutação são múltiplas expressões. Isso significa que eles não têm seu próprio tipo, mas podem ser um de vários tipos. Na maioria das vezes, as expressões lambda são usadas como expressões: s -> s + " " , podem ser Function<String, String> , mas também podem ser Function<Serializable, Object> ou UnaryOperator<String> .


Usando expressões de switch, um tipo é determinado pela interação entre o local em que o switch é usado e os tipos de suas ramificações. Se uma expressão de opção for atribuída a uma variável digitada, passada como argumento ou usada em um contexto em que o tipo exato seja conhecido (isso é chamado de tipo de destino), todas as suas ramificações deverão corresponder a esse tipo. Aqui está o que fizemos até agora:


 String result = switch (ternaryBool) { case TRUE, FALSE -> "sane"; default -> "insane"; }; 

Como resultado, a switch atribuída à variável de result do tipo String . Portanto, String é o tipo de destino e todas as ramificações devem retornar um resultado do tipo String .


A mesma coisa acontece aqui:


 Serializable serializableMessage = switch (bool) { case TRUE, FALSE -> "sane"; // note that we don't throw the exception! // but it's `Serializable`, so it matches the target type default -> new IllegalArgumentException("insane"); }; 

O que vai acontecer agora?


 // compiler infers super type of `String` and // `IllegalArgumentException` ~> `Serializable` var serializableMessage = switch (bool) { case TRUE, FALSE -> "sane"; // note that we don't throw the exception! default -> new IllegalArgumentException("insane"); }; 

(Para uso do tipo var, leia em nosso último artigo 26 recomendações para usar o tipo var em Java - observação do tradutor)


Se o tipo de destino for desconhecido, devido ao fato de usarmos var, o tipo será calculado localizando o supertipo mais específico dos tipos criados pelas ramificações.


Retorno antecipado


A consequência da diferença entre a expressão e a switch é que você pode usar return para sair da switch .


 public String sanity(Bool ternaryBool) { switch (ternaryBool) { // `return` is only possible from block case TRUE, FALSE -> { return "sane"; } default -> { return "This is ridiculous!"; } }; } 

... você não pode usar return dentro de uma expressão ...


 public String sanity(Bool ternaryBool) { String result = switch (ternaryBool) { // this does not compile - error: // "return outside of enclosing switch expression" case TRUE, FALSE -> { return "sane"; } default -> { return "This is ridiculous!"; } }; } 

Isso faz sentido se você usa uma flecha ou dois pontos.


Cobrindo todas as opções


Se você usar o switch como operador, não importa se todas as opções estão cobertas ou não. Obviamente, você pode pular o case acidentalmente e o código não funcionará corretamente, mas o compilador não se importa - você, seu IDE e suas ferramentas de análise de código ficarão apenas com isso.


As expressões de alternância exacerbam esse problema. Para onde deve mudar se o rótulo desejado estiver faltando? A única resposta que Java pode dar é retornar null para tipos de referência e um valor padrão para primitivas. Isso causaria muitos erros no código principal.


Para evitar esse resultado, o compilador pode ajudá-lo. Para instruções de opção, o compilador insistirá em que todas as opções possíveis sejam cobertas. Vejamos um exemplo que pode levar a um erro de compilação:


 // compile error: // "the switch expression does not cover all possible input values" boolean result = switch (ternaryBool) { case TRUE -> true; // no case for `FALSE` case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); }; 

A seguinte solução é interessante: adicionar a ramificação default certamente corrigirá o erro, mas essa não é a única solução - você ainda pode adicionar um case para FALSE .


 // compiles without `default` branch because // all cases for `ternaryBool` are covered boolean result = switch (ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); }; 

Sim, o compilador finalmente poderá determinar se todos os valores de enum estão cobertos (se todas as opções estão esgotadas) e não definir valores padrão inúteis! Vamos sentar um momento em gratidão silenciosa.


Embora isso ainda suscite uma pergunta. E se alguém pegar e transformar um bool louco em um booleano de quaternion, adicionando um quarto valor? Se você recompilar a expressão de opção para o Bool estendido, você receberá um erro de compilação (a expressão não é mais completa). Sem recompilação, isso se tornará um problema de tempo de execução. Para capturar esse problema, o compilador vai para a ramificação default , que se comporta da mesma forma que usamos até agora, lançando uma exceção.


No Java 12, estender todos os valores sem a ramificação default funciona apenas para enum , mas quando o switch se torna mais poderoso em versões futuras do Java, ele também pode trabalhar com tipos arbitrários. Se os rótulos de case não apenas verificarem a igualdade, mas também fizerem comparações (por exemplo, _ <5 -> ...), isso abrangerá todas as opções para tipos numéricos.


Pensando


Aprendemos com o artigo que o Java 12 transforma um switch em uma expressão, oferecendo novos recursos:


  • agora um case pode corresponder a vários rótulos;
  • O novo case … -> … forma de seta case … -> … segue a sintaxe das expressões lambda:
    • operadores de linha única ou blocos são permitidos;
    • a passagem para o próximo case impedida;
  • agora toda a expressão é avaliada como um valor, que pode ser atribuído a uma variável ou passado como parte de uma instrução maior;
  • expressão múltipla: se o tipo de destino for conhecido, todos os ramos deverão corresponder a ele. Caso contrário, é definido um tipo específico que corresponde a todos os ramos;
  • break pode retornar um valor de um bloco;
  • para uma expressão de switch usando enum , o compilador verifica o escopo de todos os seus valores. Se o default ausente, é adicionada uma ramificação que lança uma exceção.

Para onde isso nos levará? Primeiro, como essa não é a versão final do switch , você ainda terá tempo para deixar um feedback na lista de discussão Amber se não concordar com algo.


Então, supondo que o switch permaneça como está no momento, acho que a forma da seta se tornará a nova opção padrão. Sem uma passagem direta para o próximo case e com expressões lambda concisas (é muito natural ter um caso e uma instrução em uma linha), o switch parece muito mais compacto e não afeta a legibilidade do código. Tenho certeza de que usarei apenas dois pontos se precisar passar pela passagem.


O que você acha? Satisfeito com o resultado?

Source: https://habr.com/ru/post/pt443464/


All Articles