Perché non posso usare l'istruzione switch su una stringa?


1004

Questa funzionalità verrà inserita in una versione Java successiva?

Qualcuno può spiegare perché non posso farlo, come nel modo tecnico in cui switchfunziona la dichiarazione di Java ?


195
È in SE 7. 16 anni dopo la sua richiesta. download.oracle.com/javase/tutorial/java/nutsandbolts/…
angryITguy

81
Sun è stato onesto nella valutazione: "Don't hold your breath."lol, bugs.sun.com/bugdatabase/view_bug.do?bug_id=1223179
raffian

3
@raffian Penso che sia perché ha sospirato due volte. Erano in ritardo anche per rispondere, dopo quasi 10 anni. Allora avrebbe potuto impacchettare le scatole per il pranzo ai suoi nipoti.
WeirdElfB0y

Risposte:


1004

Le istruzioni switch con Stringcasi sono state implementate in Java SE 7 , almeno 16 anni dopo la loro richiesta. Non è stato fornito un chiaro motivo del ritardo, ma probabilmente ha avuto a che fare con le prestazioni.

Implementazione in JDK 7

La funzionalità è stata ora implementata javac con un processo di "riduzione dello zucchero"; una sintassi pulita e di alto livello che utilizza le Stringcostanti nelle casedichiarazioni viene espansa in fase di compilazione in codice più complesso seguendo uno schema. Il codice risultante utilizza le istruzioni JVM che sono sempre esistite.

Un caso switchcon Stringviene tradotto in due interruttori durante la compilazione. La prima mappa ogni stringa su un numero intero univoco, ovvero la sua posizione nell'interruttore originale. Questo viene fatto accendendo prima il codice hash dell'etichetta. Il caso corrispondente è ifun'istruzione che verifica l'uguaglianza delle stringhe; se ci sono collisioni sull'hash, il test è a cascata if-else-if. Il secondo interruttore rispecchia quello nel codice sorgente originale, ma sostituisce le etichette del caso con le posizioni corrispondenti. Questo processo in due passaggi semplifica la conservazione del controllo del flusso dell'interruttore originale.

Passa alla JVM

Per ulteriori approfondimenti tecnici switch, è possibile fare riferimento alla specifica JVM, in cui è descritta la compilazione delle istruzioni switch . In breve, ci sono due diverse istruzioni JVM che possono essere utilizzate per un interruttore, a seconda della scarsità delle costanti utilizzate dai casi. Entrambi dipendono dall'utilizzo di costanti intere per ciascun caso per l'esecuzione efficiente.

Se le costanti sono dense, vengono utilizzate come indice (dopo aver sottratto il valore più basso) in una tabella di puntatori di istruzioni: l' tableswitchistruzione.

Se le costanti sono sparse, viene eseguita una ricerca binaria per il caso corretto: l' lookupswitchistruzione.

In de-zuccheraggio una switchsu Stringoggetti, entrambe le istruzioni sono suscettibili di essere utilizzati. Il lookupswitchè adatto per la prima accensione codici hash per trovare la posizione originaria del caso. L'ordinale risultante è una misura naturale per a tableswitch.

Entrambe le istruzioni richiedono che le costanti intere assegnate a ciascun caso siano ordinate al momento della compilazione. In fase di esecuzione, sebbene le O(1)prestazioni di tableswitchgeneralmente appaiano migliori delle O(log(n))prestazioni di lookupswitch, sono necessarie alcune analisi per determinare se la tabella è abbastanza densa da giustificare il compromesso spazio-tempo. Bill Venners ha scritto un ottimo articolo che lo tratta in modo più dettagliato, insieme a uno sguardo nascosto ad altre istruzioni di controllo del flusso Java.

Prima di JDK 7

Prima di JDK 7, enumpoteva approssimare un Stringinterruttore basato su. Questo utilizza ilvalueOf metodo statico generato dal compilatore su ogni enumtipo. Per esempio:

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

26
Potrebbe essere più veloce usare If-Else-If invece di un hash per uno switch basato su stringhe. Ho scoperto che i dizionari sono piuttosto costosi quando si conservano solo pochi oggetti.
Jonathan Allen,

84
Un if-elseif-elseif-elseif-else potrebbe essere più veloce, ma prenderei il codice più pulito 99 volte volte su 100. Le stringhe, essendo immutabili, memorizzano nella cache il loro codice hash, quindi "calcolare" l'hash è veloce. Uno dovrebbe profilare il codice per determinare quale vantaggio ci sia.
Erickson,

21
Il motivo addotto dall'aggiunta di switch (String) è che non soddisfa le garanzie di prestazioni previste dalle istruzioni switch (). Non volevano "fuorviare" gli sviluppatori. Francamente non penso che dovrebbero garantire le prestazioni di switch () per cominciare.
Gili,

2
Se si sta semplicemente utilizzando Pillper eseguire un'azione in base a str, direi se è preferibile-else in quanto consente di gestire strvalori al di fuori dell'intervallo ROSSO, BLU senza la necessità di rilevare un'eccezione valueOfo di verificare manualmente la corrispondenza con il nome di ogni tipo di enumerazione che aggiunge un sovraccarico non necessario. Nella mia esperienza ha avuto senso usare la valueOftrasformazione in enumerazione solo se in seguito sarebbe stata necessaria una rappresentazione tipesa del valore String.
MilesHampson,

Mi chiedo se i compilatori facciano qualsiasi sforzo per testare se esiste una coppia di numeri (x, y) per i quali l'insieme di valori (hash >> x) & ((1<<y)-1)produrrebbe valori distinti per ogni stringa il cui hashCodeè diverso e che (1<<y)è inferiore al doppio del numero di stringhe (o a almeno non molto più grande di quello).
supercat

125

Se hai un posto nel tuo codice in cui puoi attivare una stringa, allora potrebbe essere meglio refactoring la stringa in modo che sia un elenco dei possibili valori, che puoi attivare. Naturalmente, limiti i potenziali valori di stringhe che puoi avere a quelli dell'enumerazione, che possono essere desiderati o meno.

Naturalmente la tua enumerazione potrebbe avere una voce per 'altro' e un metodo fromString (String), quindi potresti avere

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
Questa tecnica ti consente anche di decidere su questioni quali insensibilità al caso, alias, ecc. Invece di dipendere da un progettista del linguaggio per trovare la soluzione "taglia unica".
Darron,

2
Concordo con JeeBee, se si stanno attivando stringhe probabilmente è necessario un enum. La stringa di solito rappresenta qualcosa che va a un'interfaccia (utente o altro) che potrebbe o meno cambiare in futuro, quindi è meglio sostituirla con enums
hhafez

18
Vedi xefer.com/2006/12/switchonstring per un buon resoconto di questo metodo.
David Schmitt,

@DavidSchmitt Il write-up ha un grosso difetto. Cattura tutte le eccezioni anziché quelle che vengono effettivamente generate dal metodo.
M. Mimpen,

91

Di seguito è riportato un esempio completo basato sul post di JeeBee, che utilizza java enum invece di un metodo personalizzato.

Si noti che in Java SE 7 e versioni successive è possibile utilizzare invece un oggetto String nell'espressione dell'istruzione 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

Gli switch basati su numeri interi possono essere ottimizzati per un codice molto efficace. Gli switch basati su altri tipi di dati possono essere compilati solo in una serie di istruzioni if ​​().

Per questo motivo C & C ++ consente solo switch su tipi interi, poiché era inutile con altri tipi.

I progettisti di C # hanno deciso che lo stile era importante, anche se non c'erano vantaggi.

I progettisti di Java apparentemente pensavano come i progettisti di C.


26
Gli switch basati su qualsiasi oggetto hash possono essere implementati in modo molto efficiente usando una tabella hash - vedi .NET. Quindi la tua ragione non è completamente corretta.
Konrad Rudolph,

Sì, e questa è la cosa che non capisco. Hanno paura che gli oggetti di hashing diventino, a lungo termine, troppo costosi?
Alex Beardsley,

3
@Nalandial: in realtà, con un piccolo sforzo da parte del compilatore, non è affatto costoso perché quando si conosce l'insieme di stringhe, è abbastanza facile generare un hash perfetto (questo non è fatto da .NET; probabilmente non vale neanche la pena).
Konrad Rudolph,

3
@Nalandial & @Konrad Rudolph - Mentre l'hashing di una stringa (a causa della sua natura immutabile) sembra una soluzione a questo problema, devi ricordare che tutti gli oggetti non finali possono avere le loro funzioni di hash. Ciò rende difficile al momento della compilazione garantire la coerenza in uno switch.
martedì

2
Puoi anche costruire un DFA per abbinare la stringa (come fanno i motori delle espressioni regolari). Forse anche più efficiente dell'hashing.
Nate CK,

19

Un esempio di Stringutilizzo diretto dall'1.7 può anche essere mostrato:

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 afferma brevemente: "Gli switch basati su numeri interi possono essere ottimizzati su un codice molto efficace. Gli switch basati su altri tipi di dati possono essere compilati solo in una serie di istruzioni if ​​(). Per questo motivo C & C ++ consente solo switch su tipi interi, poiché era inutile con altri tipi ".

La mia opinione, ed è solo quella, è che non appena inizi a attivare i non primitivi devi iniziare a pensare a "uguale" rispetto a "==". In primo luogo il confronto di due stringhe può essere una procedura abbastanza lunga, che si aggiunge ai problemi di prestazioni menzionati sopra. In secondo luogo, se si stanno attivando le stringhe, ci sarà la richiesta di accendere le stringhe ignorando il caso, accendere le stringhe considerando / ignorare le impostazioni locali, accendere le stringhe in base al regex .... Approverei una decisione che ha risparmiato molto tempo per il sviluppatori di linguaggi al costo di un piccolo lasso di tempo per i programmatori.


Tecnicamente, le regex già "cambiano", dato che sono fondamentalmente solo macchine a stati; hanno semplicemente solo due "casi" matchede not matched. (Non tenendo conto di cose come [nome] gruppi / ecc., Però.)
JAB

1
docs.oracle.com/javase/7/docs/technotes/guides/language/… afferma: Il compilatore Java genera generalmente bytecode più efficiente dalle istruzioni switch che usano oggetti String che dalle istruzioni incatenate if-then-else.
Wim Deblauwe,

12

Oltre ai buoni argomenti di cui sopra, aggiungerò che molte persone oggi vedono switchcome un residuo obsoleto del passato procedurale di Java (torna ai tempi C).

Non condivido pienamente questa opinione, penso che switchpossa avere la sua utilità in alcuni casi, almeno a causa della sua velocità, e comunque è meglio di alcune serie di numeri a cascata che else ifho visto in alcuni codici ...

Ma in effetti, vale la pena esaminare il caso in cui è necessario un interruttore e vedere se non può essere sostituito da qualcosa di più OO. Ad esempio enumera in Java 1.5+, forse HashTable o qualche altra collezione (a volte rimpiango che non abbiamo funzioni (anonime) come cittadini di prima classe, come in Lua - che non ha switch - o JavaScript) o addirittura polimorfismo.


"a volte mi pento di non avere funzioni (anonime) come cittadini di prima classe" Non è più vero.
user8397947

@dorukayhan Sì, certo. Ma vuoi aggiungere un commento a tutte le risposte degli ultimi dieci anni per dire al mondo che possiamo averle se eseguiamo l'aggiornamento alle versioni più recenti di Java? :-D
PhiLho

8

Se non si utilizza JDK7 o versioni successive, è possibile utilizzare hashCode()per simularlo. Poiché di String.hashCode()solito restituisce valori diversi per stringhe diverse e restituisce sempre valori uguali per stringhe uguali, è abbastanza affidabile (stringhe diverse possono produrre lo stesso codice hash di @Lii menzionato in un commento, come "FB"e "Ea"). Vedere la documentazione .

Quindi, il codice sarebbe simile al seguente:

String s = "<Your String>";

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

In questo modo, stai tecnicamente attivando un int.

In alternativa, è possibile utilizzare il seguente codice:

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
Due stringhe diverse possono avere lo stesso hashcode, quindi se si attivano gli hashcode potrebbe essere preso il ramo di caso errato.
Lii,

@Lii Grazie per averlo segnalato! È improbabile, però, ma non mi fiderei che funzioni. "FB" ed "Ea" hanno lo stesso hashcode, quindi non è impossibile trovare una collisione. Il secondo codice è probabilmente più affidabile.
HyperNeutrino,

Sono sorpreso che queste compilazioni, poiché le casedichiarazioni dovevano, pensavo, fossero sempre valori costanti e String.hashCode()non lo sono (anche se in pratica il calcolo non è mai cambiato tra le JVM).
StaxMan,

@StaxMan Hm interessante, non ho mai smesso di osservarlo. Sì, i casevalori delle istruzioni non devono essere determinabili in fase di compilazione, quindi funziona perfettamente.
HyperNeutrino,

4

Per anni abbiamo utilizzato un preprocessore (n open source) per questo.

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

I file preelaborati sono denominati Foo.jpp e vengono elaborati in Foo.java con uno script di formica.

Il vantaggio è che viene elaborato in Java che gira su 1.0 (anche se in genere abbiamo supportato solo la versione 1.4). Inoltre, è stato molto più semplice farlo (molti switch di stringa) rispetto al confonderlo con enumerazioni o altre soluzioni alternative: il codice era molto più facile da leggere, mantenere e comprendere. IIRC (non può fornire statistiche o ragionamenti tecnici a questo punto) era anche più veloce degli equivalenti Java naturali.

Gli svantaggi sono che non stai modificando Java, quindi è un po 'più flusso di lavoro (modifica, elaborazione, compilazione / test) più un IDE si ricollegherà a Java che è un po' contorto (lo switch diventa una serie di passaggi logici if / else) e l'ordine della custodia non viene mantenuto.

Non lo consiglierei per la versione 1.7+, ma è utile se si desidera programmare Java destinato a JVM precedenti (dal momento che Joe pubblica ha raramente l'ultimo installato).

Puoi scaricarlo da SVN o sfogliare il codice online . Avrai bisogno di EBuild per costruirlo così com'è.


6
Non è necessario il JVM 1.7 per eseguire il codice con un interruttore String. Il compilatore 1.7 trasforma l'interruttore String in qualcosa che utilizza un codice byte esistente in precedenza.
Dawood ibn Kareem,

4

Altre risposte hanno detto che questo è stato aggiunto in Java 7 e fornito soluzioni alternative per le versioni precedenti. Questa risposta cerca di rispondere al "perché"

Java è stata una reazione alle complessità eccessive del C ++. È stato progettato per essere un linguaggio semplice e pulito.

String ha un po 'di gestione dei casi speciali nella lingua, ma mi sembra chiaro che i progettisti stessero cercando di ridurre al minimo la quantità di involucro speciale e zucchero sintattico.

l'accensione delle stringhe è abbastanza complessa sotto il cofano poiché le stringhe non sono semplici tipi primitivi. Non era una caratteristica comune al momento della progettazione di Java e non si adattava bene al design minimalista. Soprattutto dato che avevano deciso di non utilizzare il caso speciale == per le stringhe, sarebbe (ed è) un po 'strano che il caso funzioni dove == non funziona.

Tra 1.0 e 1.4 la lingua stessa è rimasta praticamente la stessa. La maggior parte dei miglioramenti apportati a Java erano dal lato della biblioteca.

Tutto è cambiato con Java 5, il linguaggio è stato sostanzialmente esteso. Ulteriori estensioni sono seguite nelle versioni 7 e 8. Mi aspetto che questo cambiamento di atteggiamento sia stato guidato dall'ascesa di C #


Narrativa su switch (String) si adatta a cronologia, sequenza temporale, contesto cpp / cs.
Espresso

È stato un grosso errore non implementare questa funzione, tutto il resto è una scusa economica che Java ha perso molti utenti nel corso degli anni a causa della mancanza di progressi e della testardaggine dei progettisti di non evolvere il linguaggio. Fortunatamente cambiarono completamente direzione e atteggiamento dopo il JDK7
firephil il

0

JEP 354: Switch Expressions (anteprima) in JDK-13 e JEP 361: Switch Expressions (standard) in JDK-14 estenderanno l' istruzione switch in modo che possa essere utilizzata come espressione .

Ora puoi:

  • assegnare direttamente la variabile dall'espressione switch ,
  • usa una nuova forma di etichetta switch ( case L ->):

    Il codice a destra di un'etichetta switch "case L ->" è limitato a un'espressione, un blocco o (per comodità) un'istruzione di lancio.

  • usa più costanti per caso, separate da virgole,
  • e inoltre non ci sono più interruzioni di valore :

    Per ottenere un valore da un'espressione switch, l' breakistruzione with value viene eliminata a favore di yieldun'istruzione.

Quindi la demo delle risposte ( 1 , 2 ) potrebbe apparire così:

  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

Non molto carino, ma ecco un altro modo per Java 6 e seguenti:

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

È un gioco da ragazzi a Groovy; Ho incorporato il barattolo groovy e ho creato una groovyclasse di utilità per fare tutte queste cose e altre cose che trovo esasperanti da fare in Java (dal momento che sono bloccato usando Java 6 nell'azienda.)

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

9
@SSpoke Perché questa è una domanda java e una risposta Groovy è off-topic e una spina inutile.
Martin,

12
Anche nelle grandi case conservative di SW Groovy viene usato insieme a Java. JVM offre un ambiente agnostico linguistico più che una lingua ora, per mescolare e utilizzare il paradigma di programmazione più rilevante per la soluzione. Quindi forse ora dovrei aggiungere uno snippet in Clojure per raccogliere più
voti negativi

1
Inoltre, come funziona la sintassi? Immagino che Groovy sia un linguaggio di programmazione diverso ...? Scusate. Non so niente di Groovy.
HyperNeutrino,

-4

Quando usi intellij, guarda anche:

File -> Struttura del progetto -> Progetto

File -> Struttura del progetto -> Moduli

Quando hai più moduli, assicurati di impostare il livello di lingua corretto nella scheda del modulo.


1
Non sono sicuro di come la tua risposta sia pertinente alla domanda. Ha chiesto perché non è disponibile un'istruzione switch di stringa come la seguente: String mystring = "qualcosa"; switch (mystring) {case "qualcosa" sysout ("got here"); . . }
Deepak Agarwal

-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
L'OP non chiede come attivare una stringa. Chiede perché non è in grado, a causa delle restrizioni sulla sintassi prima di JDK7.
HyperNeutrino,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.