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());
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;
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());
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());
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";
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");
É 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");
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";
O que vai acontecer agora?
(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) {
... você não pode usar return
dentro de uma expressão ...
public String sanity(Bool ternaryBool) { String result = switch (ternaryBool) {
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:
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
.
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?