Por que não consigo usar a instrução switch em uma String?


1004

Essa funcionalidade será colocada em uma versão Java posterior?

Alguém pode explicar por que não posso fazer isso, como na maneira técnica como a switchdeclaração de Java funciona?



81
Sun foi honesto em sua avaliação: "Don't hold your breath."lol, bugs.sun.com/bugdatabase/view_bug.do?bug_id=1223179
raffian

3
@raffian Eu acho que é porque ela suspirou duas vezes. Eles estavam um pouco atrasados ​​para responder também, depois de quase 10 anos. Ela poderia estar embalando lancheiras para os netos na época.
precisa saber é o seguinte

Respostas:


1004

As instruções de alternância com Stringcasos foram implementadas no Java SE 7 , pelo menos 16 anos após a primeira solicitação. Um motivo claro para o atraso não foi fornecido, mas provavelmente tinha a ver com desempenho.

Implementação no JDK 7

O recurso agora foi implementado javac com um processo de "remoção de açúcar"; uma sintaxe limpa e de alto nível usando Stringconstantes nas casedeclarações é expandida no tempo de compilação para um código mais complexo, seguindo um padrão. O código resultante usa instruções da JVM que sempre existiram.

A switchcom Stringcasos é convertido em dois comutadores durante a compilação. A primeira mapeia cada sequência para um número inteiro único - sua posição no comutador original. Isso é feito ativando primeiro o código de hash do rótulo. O caso correspondente é uma ifinstrução que testa a igualdade das cadeias de caracteres; se houver colisões no hash, o teste será em cascata if-else-if. A segunda opção espelha isso no código-fonte original, mas substitui os rótulos das caixas pelas suas posições correspondentes. Esse processo de duas etapas facilita a preservação do controle de fluxo da chave original.

Comutadores na JVM

Para obter mais detalhes técnicos switch, consulte a Especificação da JVM, na qual a compilação das instruções do switch é descrita. Em poucas palavras, existem duas instruções diferentes da JVM que podem ser usadas para um comutador, dependendo da escassez das constantes usadas pelos casos. Ambos dependem do uso de constantes inteiras para que cada caso seja executado com eficiência.

Se as constantes são densas, elas são usadas como um índice (após subtrair o valor mais baixo) em uma tabela de indicadores de instruções - a tableswitchinstrução.

Se as constantes forem escassas, uma pesquisa binária do caso correto é executada - a lookupswitchinstrução.

Em de-adoçamento um switchem Stringobjetos, ambas as instruções são susceptíveis de ser utilizados. A lookupswitché apropriado para o primeiro comutador de códigos de hash para encontrar a posição original do caso. O ordinal resultante é um ajuste natural para a tableswitch.

Ambas as instruções exigem que as constantes inteiras atribuídas a cada caso sejam classificadas em tempo de compilação. Em tempo de execução, embora o O(1)desempenho de tableswitchgeralmente pareça melhor que o O(log(n))desempenho de lookupswitch, ele requer alguma análise para determinar se a tabela é densa o suficiente para justificar a troca espaço-tempo. Bill Venners escreveu um ótimo artigo que aborda isso com mais detalhes, juntamente com uma análise detalhada de outras instruções de controle de fluxo Java.

Antes do JDK 7

Antes do JDK 7, enumera possível aproximar um Stringcomutador baseado em. Isso usa ovalueOf método estático gerado pelo compilador em todos os enumtipos. Por exemplo:

Pill p = Pill.valueOf(str);
switch(p) {
  case RED:  pop();  break;
  case BLUE: push(); break;
}

26
Pode ser mais rápido usar If-Else-If em vez de um hash para um comutador baseado em string. Eu achei os dicionários bastante caros ao armazenar apenas alguns itens.
Jonathan Allen

84
Um if-elseif-elseif-elseif-else pode ser mais rápido, mas eu usaria o código mais limpo 99 vezes em 100. As strings, por serem imutáveis, armazenam em cache o código hash, para "computar" o hash é rápido. Seria necessário criar um código de perfil para determinar qual benefício existe.
Erickson

21
O motivo dado contra a adição de switch (String) é que ele não atendia às garantias de desempenho esperadas pelas instruções switch (). Eles não queriam "enganar" os desenvolvedores. Francamente, não acho que eles devam garantir o desempenho do switch () para começar.
Gili

2
Se você está apenas usando Pillalguma ação com base em, streu argumentaria se é preferível, pois permite manipular strvalores fora do intervalo VERMELHO, AZUL sem a necessidade de capturar uma exceção valueOfou verificar manualmente uma correspondência com o nome de cada tipo de enumeração que apenas adiciona sobrecarga desnecessária. Na minha experiência, só fazia sentido usar valueOfpara se transformar em enumeração se uma representação segura do tipo String fosse necessária posteriormente.
MilesHampson

Gostaria de saber se os compiladores fazem algum esforço para testar se existe algum par de números (x, y) para o qual o conjunto de valores (hash >> x) & ((1<<y)-1)produziria valores distintos para cada string hashCodediferente e (1<<y)menor que o dobro do número de strings (ou em pelo menos, não muito maior que isso).
Supercat

125

Se você tem um lugar no seu código onde pode ativar uma String, pode ser melhor refatorá-la para ser uma enumeração dos valores possíveis, que você pode ativar. Obviamente, você limita os valores potenciais de Strings que você pode ter aos da enumeração, que podem ou não ser desejados.

É claro que sua enumeração pode ter uma entrada para 'other' e um método fromString (String), então você pode ter

ValueEnum enumval = ValueEnum.fromString(myString);
switch (enumval) {
   case MILK: lap(); break;
   case WATER: sip(); break;
   case BEER: quaff(); break;
   case OTHER: 
   default: dance(); break;
}

4
Essa técnica também permite que você decida sobre questões como distinção entre maiúsculas e minúsculas, aliases etc. Em vez de depender de um designer de linguagem para encontrar a solução "tamanho único".
Darron

2
Concorde com o JeeBee, se você estiver ativando strings, provavelmente precisará de uma enumeração. A seqüência de caracteres geralmente representa algo indo para uma interface (usuário ou não) que podem ou não mudar no futuro então é melhor substituí-lo com enums
hhafez

18
Consulte xefer.com/2006/12/switchonstring para obter uma boa descrição deste método.
David Schmitt

@DavidSchmitt O artigo tem uma grande falha. Ele captura todas as exceções, em vez das que são realmente lançadas pelo método.
M. Mimpen

91

A seguir, é apresentado um exemplo completo, com base na postagem de JeeBee, usando java enum em vez de usar um método personalizado.

Observe que no Java SE 7 e posterior, você pode usar um objeto String na expressão da instrução switch.

public class Main {

    /**
    * @param args the command line arguments
    */
    public static void main(String[] args) {

      String current = args[0];
      Days currentDay = Days.valueOf(current.toUpperCase());

      switch (currentDay) {
          case MONDAY:
          case TUESDAY:
          case WEDNESDAY:
              System.out.println("boring");
              break;
          case THURSDAY:
              System.out.println("getting better");
          case FRIDAY:
          case SATURDAY:
          case SUNDAY:
              System.out.println("much better");
              break;

      }
  }

  public enum Days {

    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
  }
}

26

Os comutadores baseados em números inteiros podem ser otimizados para um código muito eficaz. Os comutadores baseados em outro tipo de dados só podem ser compilados em uma série de instruções if ().

Por esse motivo, o C & C ++ permite apenas comutações em tipos inteiros, pois não faz sentido em outros tipos.

Os designers de C # decidiram que o estilo era importante, mesmo que não houvesse vantagem.

Os designers de Java aparentemente pensavam como os designers de C.


26
Switches baseados em qualquer objeto hashável podem ser implementados com muita eficiência usando uma tabela de hash - consulte .NET. Portanto, seu motivo não está completamente correto.
Konrad Rudolph

Sim, e é isso que eu não entendo. Eles temem que os objetos hash se tornem, a longo prazo, muito caros?
Alex Beardsley

3
@Nalandial: na verdade, com um pouco de esforço por parte do compilador, não é caro, porque quando o conjunto de strings é conhecido, é muito fácil gerar um hash perfeito (isso não é feito pelo .NET; provavelmente também não vale o esforço).
Konrad Rudolph

3
@Nalandial e @Konrad Rudolph - Embora o hash de uma String (devido à sua natureza imutável) pareça uma solução para esse problema, você deve se lembrar que todos os Objetos não finais podem ter suas funções de hash substituídas. Isso torna difícil no momento da compilação garantir a consistência em um comutador.
martinatime 3/08/08

2
Você também pode construir um DFA para corresponder à sequência (como os mecanismos de expressão regular). Possivelmente ainda mais eficiente que o hash.
Nate CK

19

Um exemplo de Stringuso direto desde 1.7 pode ser mostrado também:

public static void main(String[] args) {

    switch (args[0]) {
        case "Monday":
        case "Tuesday":
        case "Wednesday":
            System.out.println("boring");
            break;
        case "Thursday":
            System.out.println("getting better");
        case "Friday":
        case "Saturday":
        case "Sunday":
            System.out.println("much better");
            break;
    }

}

18

James Curran diz sucintamente: "Os comutadores baseados em números inteiros podem ser otimizados para um código muito eficaz. Os comutadores baseados em outro tipo de dados só podem ser compilados em uma série de instruções if (). Por esse motivo, o C & C ++ permite apenas comutadores em tipos inteiros, já que era inútil com outros tipos ".

Minha opinião, e é só isso, é que, assim que você começa a ativar os não-primitivos, você precisa começar a pensar em "iguais" versus "==". Em primeiro lugar, comparar duas seqüências de caracteres pode ser um procedimento bastante demorado, aumentando os problemas de desempenho mencionados acima. Em segundo lugar, se houver a ativação de strings, haverá uma demanda por ativação de strings ignorando maiúsculas e minúsculas, ativação de strings considerando / ignorando localidade, ativação de strings com base em regex .... Eu aprovaria uma decisão que economizasse muito tempo para o desenvolvedores de idiomas ao custo de uma pequena quantidade de tempo para programadores.


Tecnicamente, as expressões regulares já "alternam", pois são basicamente apenas máquinas de estado; eles apenas têm apenas dois "casos", matchede not matched. (Não levando em conta coisas como [chamado] grupos / etc, no entanto..)
JAB

1
docs.oracle.com/javase/7/docs/technotes/guides/language/… afirma: O compilador Java gera geralmente bytecode mais eficiente a partir de instruções de switch que usam objetos String do que a partir de instruções encadeadas if-then-else.
Wim Deblauwe

12

Além dos bons argumentos acima, acrescentarei que muitas pessoas hoje veem switchcomo um restante obsoleto do passado processual do Java (de volta aos tempos C).

Não concordo totalmente com essa opinião, acho que switchpode ter sua utilidade em alguns casos, pelo menos por causa de sua velocidade, e de qualquer maneira é melhor do que algumas séries de números numéricos em cascata else ifque vi em algum código ...

Mas, de fato, vale a pena examinar o caso em que você precisa de um switch e ver se ele não pode ser substituído por algo mais OO. Por exemplo, enums em Java 1.5+, talvez HashTable ou alguma outra coleção (em algum momento me arrependo de não termos funções (anônimas) como cidadão de primeira classe, como em Lua - que não possui switch - ou JavaScript) ou mesmo polimorfismo.


"em algum momento lamento que não tenhamos funções (anônimas) como cidadão de primeira classe" Isso não é mais verdade.
precisa saber é o seguinte

@dorukayhan Sim, é claro. Mas você deseja adicionar um comentário a todas as respostas dos últimos dez anos para dizer ao mundo que podemos tê-las se atualizarmos para versões mais recentes do Java? :-D
PhiLho

8

Se você não estiver usando o JDK7 ou superior, poderá usá hashCode()-lo para simular. Como String.hashCode()geralmente retorna valores diferentes para cadeias diferentes e sempre retorna valores iguais para cadeias iguais, é bastante confiável (cadeias diferentes podem produzir o mesmo código de hash que o @Lii mencionado em um comentário, como "FB"e "Ea") Consulte a documentação .

Portanto, o código ficaria assim:

String s = "<Your String>";

switch(s.hashCode()) {
case "Hello".hashCode(): break;
case "Goodbye".hashCode(): break;
}

Dessa forma, você está tecnicamente ativando um int.

Como alternativa, você pode usar o seguinte código:

public final class Switch<T> {
    private final HashMap<T, Runnable> cases = new HashMap<T, Runnable>(0);

    public void addCase(T object, Runnable action) {
        this.cases.put(object, action);
    }

    public void SWITCH(T object) {
        for (T t : this.cases.keySet()) {
            if (object.equals(t)) { // This means that the class works with any object!
                this.cases.get(t).run();
                break;
            }
        }
    }
}

5
Duas cadeias diferentes podem ter o mesmo código de hash, portanto, se você ativar códigos de hash, poderá ser utilizada a ramificação de caso incorreta.
Lii 23/01

@ Lii Obrigado por apontar isso! É improvável, porém, mas eu não confiaria que funcionasse. "FB" e "Ea" têm o mesmo código de hash, portanto, não é impossível encontrar uma colisão. O segundo código é provavelmente mais confiável.
HyperNeutrino

Surpreende-me que essas compilações, como as casedeclarações devam, pensei, sempre sejam valores constantes, e String.hashCode()não sejam tais (mesmo que, na prática, o cálculo nunca tenha mudado entre as JVMs).
precisa saber é

@StaxMan Hm interessante, nunca parei para observar isso. Mas sim, os casevalores das instruções não precisam ser determináveis ​​no tempo de compilação, para que funcione com precisão.
HyperNeutrino

4

Por anos, usamos um pré-processador (de código aberto) para isso.

//#switch(target)
case "foo": code;
//#end

Os arquivos pré-processados ​​são nomeados Foo.jpp e são processados ​​no Foo.java com um script ant.

A vantagem é que ele é processado em Java que roda na versão 1.0 (embora normalmente só tenhamos suporte na versão 1.4). Além disso, era muito mais fácil fazer isso (muitas opções de cadeias de caracteres) em comparação com enums ou outras soluções alternativas - o código era muito mais fácil de ler, manter e entender. O IIRC (não é possível fornecer estatísticas ou raciocínio técnico neste momento) também foi mais rápido que os equivalentes naturais de Java.

As desvantagens são que você não está editando Java, por isso é um pouco mais de fluxo de trabalho (editar, processar, compilar / testar), mais um IDE será vinculado ao Java que é um pouco complicado (a opção se torna uma série de etapas lógicas if / else) e a ordem das caixas de comutação não é mantida.

Eu não o recomendaria para a versão 1.7+, mas é útil se você deseja programar o Java direcionado a JVMs anteriores (já que o público de Joe raramente tem a última versão instalada).

Você pode obtê-lo no SVN ou procurar o código online . Você precisará do EBuild para construí-lo como está.


6
Você não precisa da JVM 1.7 para executar o código com um comutador String. O compilador 1.7 transforma a opção String em algo que usa código de bytes existente anteriormente.
Dawood ibn Kareem

4

Outras respostas disseram que isso foi adicionado no Java 7 e deu soluções alternativas para versões anteriores. Esta resposta tenta responder o "porquê"

Java foi uma reação às complexidades excessivas do C ++. Foi projetado para ser uma linguagem simples e limpa.

String teve um pouco de tratamento especial de caso na linguagem, mas parece claro para mim que os designers estavam tentando reduzir ao mínimo a quantidade de invólucro especial e açúcar sintático.

ligar as cordas é bastante complexo sob o capô, pois as cordas não são tipos primitivos simples. Não era um recurso comum no momento em que o Java foi projetado e realmente não se encaixa bem no design minimalista. Especialmente porque eles decidiram não usar o caso especial == para strings, seria (e é) um pouco estranho para o caso trabalhar onde == não.

Entre 1.0 e 1.4, a própria linguagem permaneceu praticamente a mesma. A maioria dos aprimoramentos do Java estava no lado da biblioteca.

Tudo isso mudou com o Java 5, a linguagem foi substancialmente estendida. Outras extensões foram seguidas nas versões 7 e 8. Espero que essa mudança de atitude tenha sido impulsionada pelo aumento do C #


A narrativa sobre switch (String) se encaixa com a história, linha do tempo, contexto cpp / cs.
Espresso

Foi um grande erro não implementar esse recurso, tudo o mais é uma desculpa barata. O Java perdeu muitos usuários ao longo dos anos por causa da falta de progresso e da teimosia dos designers em não evoluir a linguagem. Felizmente que mudou completamente a direção e atitude após JDK7
firephil

0

O JEP 354: Expressões do switch (visualização) no JDK-13 e o JEP 361: Expressões do switch (padrão) no JDK-14 estenderão a instrução switch para que ele possa ser usado como expressão .

Agora você pode:

  • atribuir diretamente variável da expressão do comutador ,
  • use uma nova forma de etiqueta de chave ( case L ->):

    O código à direita de um rótulo de opção "case L ->" é restrito a ser uma expressão, um bloco ou (por conveniência) uma instrução throw.

  • use várias constantes por caso, separadas por vírgulas,
  • e também não há mais quebras de valor :

    Para gerar um valor a partir de uma expressão de opção, a breakinstrução with value é descartada em favor de uma yieldinstrução.

Portanto, a demonstração das respostas ( 1 , 2 ) pode ficar assim:

  public static void main(String[] args) {
    switch (args[0]) {
      case "Monday", "Tuesday", "Wednesday" ->  System.out.println("boring");
      case "Thursday" -> System.out.println("getting better");
      case "Friday", "Saturday", "Sunday" -> System.out.println("much better");
    }

-2

Não é muito bonito, mas aqui está outra maneira para o Java 6 e abaixo:

String runFct = 
        queryType.equals("eq") ? "method1":
        queryType.equals("L_L")? "method2":
        queryType.equals("L_R")? "method3":
        queryType.equals("L_LR")? "method4":
            "method5";
Method m = this.getClass().getMethod(runFct);
m.invoke(this);

-3

É uma brisa no Groovy; Incorporei o groovy jar e criei uma groovyclasse de utilitário para fazer todas essas coisas e muito mais que acho exasperante em Java (já que estou preso usando o Java 6 na empresa).

it.'p'.each{
switch (it.@name.text()){
   case "choclate":
     myholder.myval=(it.text());
     break;
     }}...

9
@SSpoke Porque esta é uma pergunta sobre Java e uma resposta do Groovy é off-topic e um plugue inútil.
Martin

12
Mesmo em grandes casas conservadoras de SW, o Groovy é usado junto com o Java. A JVM oferece um ambiente independente de linguagem mais do que uma linguagem agora, para misturar e usar o paradigma de programação mais relevante para a solução. Então talvez agora eu deveria adicionar um trecho em Clojure para reunir mais downvotes :) ...
Alex Punnen

1
Além disso, como a sintaxe funciona? Eu estou supondo que Groovy é uma linguagem de programação diferente ...? Desculpa. Não sei nada sobre o Groovy.
HyperNeutrino 26/01

-4

Ao usar o intellij, observe também:

Arquivo -> Estrutura do projeto -> Projeto

Arquivo -> Estrutura do Projeto -> Módulos

Quando você possui vários módulos, certifique-se de definir o nível de idioma correto na guia módulo.


1
Não tenho certeza de como sua resposta é relevante para a pergunta. Ele perguntou por que uma declaração de troca de string como a seguinte não está disponível: String mystring = "something"; switch (mystring) {case "algo" sysout ("cheguei aqui"); . . }
Deepak Agarwal 14/11

-8
public class StringSwitchCase { 

    public static void main(String args[]) {

        visitIsland("Santorini"); 
        visitIsland("Crete"); 
        visitIsland("Paros"); 

    } 

    public static void visitIsland(String island) {
         switch(island) {
          case "Corfu": 
               System.out.println("User wants to visit Corfu");
               break; 
          case "Crete": 
               System.out.println("User wants to visit Crete");
               break; 
          case "Santorini": 
               System.out.println("User wants to visit Santorini");
               break; 
          case "Mykonos": 
               System.out.println("User wants to visit Mykonos");
               break; 
         default: 
               System.out.println("Unknown Island");
               break; 
         } 
    } 

} 

8
O OP não está perguntando como ativar uma string. Ele / ela está perguntando por que ele / ela não é capaz, devido a restrições de sintaxe antes do JDK7.
HyperNeutrino 02/07/2015
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.