Dlaczego nie mogę użyć instrukcji switch na łańcuchu?

1004

Czy ta funkcjonalność zostanie wprowadzona w późniejszej wersji Java?

Czy ktoś może wyjaśnić, dlaczego nie mogę tego zrobić, tak jak w technicznym sposobie działania switchinstrukcji Java ?

Alex Beardsley
źródło
195
Jest w SE 7. 16 lat od jego żądania. download.oracle.com/javase/tutorial/java/nutsandbolts/…
angryITguy
81
Firma Sun była uczciwa w swojej ocenie: "Don't hold your breath."lol, bugs.sun.com/bugdatabase/view_bug.do?bug_id=1223179
raffian
3
@raffian Myślę, że to dlatego, że westchnęła dwa razy. Spóźnili się też z odpowiedzią, po prawie 10 latach. Mogła wtedy pakować pudełka na lunch do wnuków.
WeirdElfB0y

Odpowiedzi:

1003

Deklaracje zamiany z Stringprzypadkami zostały zaimplementowane w Javie SE 7 , co najmniej 16 lat po ich pierwszym zgłoszeniu. Nie podano wyraźnego powodu opóźnienia, ale prawdopodobnie miało to związek z wydajnością.

Wdrożenie w JDK 7

Funkcja została teraz zaimplementowana w javac procesie „usuwania cukru”; czysta, wysokopoziomowa składnia wykorzystująca Stringstałe w casedeklaracjach jest rozszerzana w czasie kompilacji do bardziej złożonego kodu zgodnie ze wzorcem. Wynikowy kod używa instrukcji JVM, które zawsze istniały.

A switchz Stringprzypadkami jest tłumaczone na dwa przełączniki podczas kompilacji. Pierwszy mapuje każdy ciąg na unikalną liczbę całkowitą - jego pozycję w oryginalnym przełączniku. Odbywa się to poprzez pierwsze włączenie kodu skrótu etykiety. Odpowiednim przypadkiem jest ifinstrukcja, która testuje równość łańcucha; jeśli w haszu występują kolizje, test jest kaskadowy if-else-if. Drugi przełącznik odzwierciedla to w oryginalnym kodzie źródłowym, ale zastępuje etykiety liter odpowiednimi pozycjami. Ten dwuetapowy proces ułatwia zachowanie kontroli przepływu oryginalnego przełącznika.

Przełączniki w JVM

Aby uzyskać więcej szczegółów technicznych switch, można zapoznać się ze specyfikacją JVM, w której opisano kompilację instrukcji switch . Krótko mówiąc, istnieją dwie różne instrukcje JVM, których można użyć dla przełącznika, w zależności od rzadkości stałych używanych przez przypadki. Oba zależą od wykorzystania stałych liczb całkowitych dla każdego przypadku, aby wykonać je skutecznie.

Jeśli stałe są gęste, są one używane jako indeks (po odjęciu najniższej wartości) do tabeli wskaźników instrukcji - tableswitchinstrukcji.

Jeśli stałe są rzadkie, przeprowadzane jest binarne wyszukiwanie poprawnej wielkości liter - lookupswitchinstrukcja.

W odcukrzania switchna Stringobiektach, obie instrukcje mogą być użyte. lookupswitchNadaje pierwszy przełącznik na kody hash znaleźć oryginalne stanowisko w sprawie. Wynikowy porządek jest naturalnym dopasowaniem do tableswitch.

Obie instrukcje wymagają sortowania stałych liczb całkowitych przypisanych do każdego przypadku w czasie kompilacji. W czasie wykonywania O(1)wydajność na tableswitchogół wydaje się lepsza niż O(log(n))wydajność lookupswitch, ale wymaga pewnej analizy w celu ustalenia, czy tabela jest wystarczająco gęsta, aby uzasadnić kompromis czasoprzestrzenny. Bill Venners napisał świetny artykuł, który omawia to bardziej szczegółowo, wraz z ukrytym spojrzeniem na inne instrukcje kontroli przepływu Java.

Przed JDK 7

Przed JDK 7 enummógł w przybliżeniu Stringprzełącznik oparty na przełączniku. Wykorzystuje statycznąvalueOf metodę generowaną przez kompilator dla każdego enumtypu. Na przykład:

Pill p = Pill.valueOf(str);
switch(p) {
  case RED:  pop();  break;
  case BLUE: push(); break;
}
erickson
źródło
26
Może być szybsze użycie po prostu If-Else-If zamiast skrótu dla przełącznika opartego na łańcuchach. Odkryłem, że słowniki są dość drogie, gdy przechowuję tylko kilka przedmiotów.
Jonathan Allen,
84
If-elseif-elseif-elseif-else może być szybszy, ale wziąłbym czystszy kod 99 razy na 100. Ciągi, ponieważ są niezmienne, buforują swój kod skrótu, więc „obliczanie” skrótu jest szybkie. Trzeba będzie profilować kod, aby ustalić, jakie korzyści to ma.
erickson,
21
Podany powód odmowy dodania przełącznika (String) jest taki, że nie spełniałby on gwarancji wydajności, których oczekuje się od instrukcji switch (). Nie chcieli „wprowadzać w błąd” programistów. Szczerze mówiąc, nie sądzę, że powinny one na początek gwarantować wydajność switch ().
Gili
2
Jeśli używasz tylko Pilldo podjęcia pewnych działań w oparciu o str, argumentowałbym, czy preferowane jest użycie opcji else-else, ponieważ pozwala ona obsługiwać strwartości spoza zakresu CZERWONEGO, NIEBIESKIEGO bez potrzeby wychwytywania wyjątku valueOflub ręcznego sprawdzania zgodności z nazwą każdy typ wyliczenia, który po prostu dodaje niepotrzebne koszty ogólne. Z mojego doświadczenia wynika, że valueOfprzekształcenie w wyliczenie ma sens tylko wtedy, gdy później potrzebna będzie bezpieczna reprezentacja wartości ciągu.
MilesHampson,
Zastanawiam się, czy kompilatory starają się sprawdzić, czy istnieje jakakolwiek para liczb (x, y), dla której zestaw wartości (hash >> x) & ((1<<y)-1)dałby różne wartości dla każdego łańcucha, który hashCodejest inny i (1<<y)jest mniejszy niż dwukrotność liczby łańcuchów (lub w przynajmniej niewiele więcej niż to).
supercat
125

Jeśli masz w kodzie miejsce, w którym możesz włączyć String, może być lepiej refaktoryzować String, aby był wyliczeniem możliwych wartości, które możesz włączyć. Oczywiście ograniczasz potencjalne wartości Ciągów, które możesz mieć, do tych z wyliczenia, które mogą być pożądane lub nie.

Oczywiście twoje wyliczenie może mieć wpis dla „other” oraz metodę fromString (String), wtedy możesz mieć

ValueEnum enumval = ValueEnum.fromString(myString);
switch (enumval) {
   case MILK: lap(); break;
   case WATER: sip(); break;
   case BEER: quaff(); break;
   case OTHER: 
   default: dance(); break;
}
JeeBee
źródło
4
Ta technika pozwala również decydować o takich kwestiach, jak rozróżnianie wielkości liter, aliasy itp. Zamiast zależeć od projektanta języka, aby zaproponować rozwiązanie „jeden rozmiar dla wszystkich”.
Darron,
2
Zgadzam się z JeeBee, jeśli włączasz ciągi, prawdopodobnie potrzebujesz wyliczenia. Ciąg zwykle reprezentuje coś, co przechodzi do interfejsu (użytkownika lub w inny sposób), co może ulec zmianie w przyszłości, więc lepiej zastąp go
wyliczeniami
18
Zobacz xefer.com/2006/12/switchonstring, aby uzyskać miły opis tej metody.
David Schmitt
@DavidSchmitt Pisanie ma jedną poważną wadę. Łapie wszystkie wyjątki zamiast tych, które są faktycznie zgłaszane przez metodę.
M. Mimpen
91

Poniżej znajduje się kompletny przykład oparty na poście JeeBee, w którym zastosowano enum java zamiast metody niestandardowej.

Zauważ, że w Javie SE 7 i nowszych można użyć obiektu String w wyrażeniu instrukcji 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
  }
}
Thulani Chivandikwa
źródło
26

Przełączniki oparte na liczbach całkowitych można zoptymalizować pod kątem bardzo skutecznego kodu. Przełączniki oparte na innym typie danych można skompilować tylko do szeregu instrukcji if ().

Z tego powodu C & C ++ zezwala tylko na przełączniki na liczbach całkowitych, ponieważ nie miało to sensu w przypadku innych typów.

Projektanci C # zdecydowali, że styl jest ważny, nawet jeśli nie ma żadnej przewagi.

Projektanci Javy najwyraźniej myśleli jak projektanci C.

James Curran
źródło
26
Przełączniki oparte na dowolnym haszowalnym obiekcie mogą być bardzo skutecznie implementowane przy użyciu tablicy haszującej - patrz .NET. Więc twój powód nie jest całkowicie poprawny.
Konrad Rudolph,
Tak, a tego nie rozumiem. Czy obawiają się, że obiekty haszujące na dłuższą metę staną się zbyt drogie?
Alex Beardsley,
3
@Nalandial: właściwie, przy niewielkim wysiłku ze strony kompilatora, wcale nie jest to drogie, ponieważ kiedy znany jest zestaw ciągów, dość łatwo jest wygenerować idealny skrót (jednak nie jest to zrobione przez .NET; prawdopodobnie też nie warte wysiłku).
Konrad Rudolph,
3
@Nalandial & @Konrad Rudolph - Podczas gdy haszowanie łańcucha (ze względu na jego niezmienną naturę) wydaje się rozwiązaniem tego problemu, musisz pamiętać, że wszystkie nie-końcowe obiekty mogą mieć nadpisane funkcje haszujące. Utrudnia to w czasie kompilacji zapewnienie spójności przełącznika.
martinatime,
2
Możesz również zbudować DFA, aby dopasować ciąg (tak jak robią to silniki wyrażeń regularnych). Być może nawet bardziej wydajny niż mieszanie.
Nate CK
19

Przykład bezpośredniego Stringużycia od wersji 1.7 może być również pokazany:

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;
    }

}
Gunnar Forsgren - Mobimacja
źródło
18

James Curran zwięźle mówi: „Przełączniki oparte na liczbach całkowitych można zoptymalizować pod kątem bardzo skutecznego kodu. Przełączniki oparte na innym typie danych można skompilować tylko do szeregu instrukcji if (). Z tego powodu C i C ++ zezwalają tylko na przełączniki na liczbach całkowitych, ponieważ nie miało to sensu w przypadku innych typów ”.

Moim zdaniem i tylko tyle, że jak tylko zaczniesz włączać nie-prymitywy, musisz zacząć myśleć o „równa się” w porównaniu z „==”. Po pierwsze, porównanie dwóch ciągów może być dość długą procedurą, zwiększając problemy z wydajnością, o których mowa powyżej. Po drugie, jeśli istnieje włączanie ciągów, będzie zapotrzebowanie na włączenie ciągów ignorujących wielkość liter, włączenie ciągów z uwzględnieniem / ignorowanie ustawień regionalnych, włączenie ciągów opartych na wyrażeniu regularnym ... Chciałbym zatwierdzić decyzję, która zaoszczędziłaby dużo czasu na programistów języków kosztem niewielkiej ilości czasu dla programistów.

DJClayworth
źródło
Technicznie rzecz biorąc, wyrażenia regularne już „przełączają się”, ponieważ są to w zasadzie tylko maszyny stanów; mają tylko dwa „przypadki” matchedi not matched. (Nie biorąc jednak pod uwagę takich rzeczy, jak [nazwane] grupy / itp.)
JAB
1
docs.oracle.com/javase/7/docs/technotes/guides/language/… stwierdza: Kompilator Java generuje ogólnie bardziej wydajny kod bajtowy z instrukcji switch, które używają obiektów String, niż z łańcuchowych instrukcji if-then-else.
Wim Deblauwe
12

Oprócz powyższych dobrych argumentów dodam, że wielu ludzi postrzega dziś switchjako przestarzałą pozostałość proceduralnej przeszłości Jawy (do czasów C).

Nie podzielam w pełni tej opinii, myślę, że switchmoże być przydatna w niektórych przypadkach, przynajmniej ze względu na jej szybkość, a poza tym jest lepsza niż pewna seria kaskadowych liczb, else ifktóre widziałem w jakimś kodzie ...

Ale rzeczywiście warto spojrzeć na przypadek, w którym potrzebujesz przełącznika, i sprawdź, czy nie można go zastąpić czymś więcej OO. Na przykład wyliczenia w Javie 1.5+, być może HashTable lub innej kolekcji (czasami żałuję, że nie mamy (anonimowych) funkcji jako obywatele pierwszej klasy, jak w Lua - która nie ma przełącznika - lub JavaScript), a nawet polimorfizmu.

PhiLho
źródło
„kiedyś żałuję, że nie mamy (anonimowych) funkcji jako obywatela pierwszej klasy” To już nie prawda.
user8397947,
@dorukayhan Tak, oczywiście. Ale czy chcesz dodać komentarz do wszystkich odpowiedzi z ostatnich dziesięciu lat, aby powiedzieć światu, że możemy je mieć, jeśli zaktualizujemy do nowszych wersji Java? :-D
PhiLho
8

Jeśli nie używasz JDK7 lub nowszej wersji, możesz użyć jej hashCode()do symulacji. Ponieważ String.hashCode()zwykle zwraca różne wartości dla różnych ciągów i zawsze zwraca równe wartości dla równych ciągów, jest dość wiarygodny (Różne ciągi mogą wytwarzać ten sam kod skrótu, jak @Lii wspomniany w komentarzu, taki jak "FB"i "Ea") Zobacz dokumentację .

Tak więc kod wyglądałby tak:

String s = "<Your String>";

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

W ten sposób technicznie włączasz int.

Alternatywnie możesz użyć następującego kodu:

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;
            }
        }
    }
}
HyperNeutrino
źródło
5
Dwa różne ciągi znaków mogą mieć ten sam kod skrótu, więc jeśli włączysz kody skrótu, może zostać pobrana niewłaściwa gałąź-rozróżnienia.
Lii
@Lii Dzięki za zwrócenie na to uwagi! Jest to jednak mało prawdopodobne, ale nie ufałbym, że zadziała. „FB” i „Ea” mają ten sam kod skrótu, więc nie jest niemożliwe znalezienie kolizji. Drugi kod jest prawdopodobnie bardziej niezawodny.
HyperNeutrino
Dziwię się, że te kompilacje są, jak casesądzę, stwierdzenia, które zawsze muszą być stałymi wartościami, i String.hashCode()takie nie są (nawet jeśli w praktyce obliczenia nigdy nie zmieniły się między JVM).
StaxMan
@StaxMan Hm ciekawe, nigdy nie przestałem tego obserwować. Ale tak, casewartości instrukcji nie muszą być możliwe do ustalenia w czasie kompilacji, więc działają dobrze.
HyperNeutrino
4

Od lat używamy do tego preprocesora (open source).

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

Wstępnie przetworzone pliki są nazywane Foo.jpp i przetwarzane do Foo.java za pomocą skryptu ant.

Zaletą jest to, że jest przetwarzany na Javę działającą w wersji 1.0 (chociaż zwykle obsługiwaliśmy tylko powrót do wersji 1.4). Znacznie łatwiej było to zrobić (wiele przełączników łańcuchowych) w porównaniu do przerabiania go za pomocą wyliczeń lub innych obejść - kod był o wiele łatwiejszy do odczytania, utrzymania i zrozumienia. IIRC (w tym momencie nie może dostarczyć statystyk ani uzasadnienia technicznego) było także szybsze niż naturalne odpowiedniki Javy.

Wady polegają na tym, że nie edytujesz Javy, więc jest to nieco większy przepływ pracy (edycja, przetwarzanie, kompilacja / test), a IDE połączy się ponownie z Javą, która jest nieco skomplikowana (zmiana staje się serią logicznych kroków if / else) a kolejność skrzynki przełączników nie jest zachowana.

Nie polecałbym tego dla wersji 1.7+, ale jest przydatny, jeśli chcesz programować Javę, która jest ukierunkowana na wcześniejsze maszyny JVM (ponieważ Joe public rzadko ma najnowszą zainstalowaną wersję).

Możesz go pobrać z SVN lub przeglądać kod online . Będziesz potrzebował EBuild, aby zbudować go takim, jakim jest.

Charles Goodwin
źródło
6
Nie potrzebujesz JVM 1.7 do uruchamiania kodu za pomocą przełącznika String. Kompilator 1.7 przekształca przełącznik String w coś, co używa wcześniej istniejącego kodu bajtowego.
Dawood ibn Kareem,
4

Inne odpowiedzi mówiły, że zostało to dodane w Javie 7 i podano obejścia dla wcześniejszych wersji. Ta odpowiedź próbuje odpowiedzieć na pytanie „dlaczego”

Java była reakcją na nadmierną złożoność C ++. Został zaprojektowany jako prosty, czysty język.

String zyskał trochę specjalnej obsługi skrzynek w tym języku, ale wydaje mi się jasne, że projektanci starali się ograniczyć ilość specjalnej obudowy i cukru syntaktycznego do minimum.

włączanie łańcuchów jest dość skomplikowane pod maską, ponieważ łańcuchy nie są prostymi typami prymitywnymi. Nie była to powszechna funkcja w czasie projektowania Java i tak naprawdę nie pasuje do minimalistycznego wyglądu. Zwłaszcza, że ​​zdecydowali się nie używać specjalnego case == dla ciągów, byłoby (i jest) trochę dziwne, aby case działał tam, gdzie == nie.

Między 1.0 a 1.4 sam język pozostał prawie taki sam. Większość ulepszeń w Javie była po stronie biblioteki.

Wszystko zmieniło się wraz z Javą 5, język został znacznie rozszerzony. Kolejne rozszerzenia pojawiły się w wersjach 7 i 8. Oczekuję, że ta zmiana nastawienia była spowodowana wzrostem C #

płyn do płukania
źródło
Narracja na temat przełącznika (ciąg) pasuje do historii, osi czasu, kontekstu cpp / cs.
Espresso
Wielkim błędem było nie wdrożyć tej funkcji, wszystko inne jest tanią wymówką, że Java straciła wielu użytkowników na przestrzeni lat z powodu braku postępu i uporu projektantów, aby nie ewoluować językiem. Na szczęście całkowicie zmienili kierunek i nastawienie po JDK7
firephil
0

JEP 354: Przełącz wyrażenia ( wersja zapoznawcza) w JDK-13 i JEP 361: Przełącz wyrażenia (Standard) w JDK-14 rozszerzy instrukcję przełączania, aby mogła być używana jako wyrażenie .

Teraz możesz:

  • bezpośrednio przypisać zmienną z wyrażenia przełączającego ,
  • użyj nowej formy przełącznika label ( case L ->):

    Kod po prawej stronie etykiety przełącznika „case L ->” jest ograniczony do wyrażenia, bloku lub (dla wygody) instrukcji throw.

  • użyj wielu stałych na przypadek, oddzielonych przecinkami,
  • a także nie ma już podziałów wartości :

    Aby uzyskać wartość z wyrażenia przełączającego, instrukcja breakwith value jest upuszczana na korzyść yieldinstrukcji.

Demo z odpowiedzi ( 1 , 2 ) może wyglądać następująco:

  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");
    }
Iskuskov Alexander
źródło
-2

Niezbyt ładna, ale oto inny sposób dla Java 6 i poniżej:

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);
Conete Cristian
źródło
-3

W Groovy to bryza; Osadzam groovy jar i tworzę groovyklasę narzędziową do robienia wszystkich tych rzeczy i więcej, co jest dla mnie irytujące w Javie (ponieważ utknąłem przy użyciu Java 6 w przedsiębiorstwie).

it.'p'.each{
switch (it.@name.text()){
   case "choclate":
     myholder.myval=(it.text());
     break;
     }}...
Alex Punnen
źródło
9
@SSpoke Ponieważ jest to pytanie Java, a odpowiedź Groovy jest nie na temat i jest bezużyteczną wtyczką.
Martin
12
Nawet w konserwatywnych dużych domach SW Groovy jest używany wraz z Javą. JVM daje teraz środowisko agnostyczne dla języka więcej niż język, aby mieszać i stosować najbardziej odpowiedni paradygmat programowania dla rozwiązania. Więc może teraz powinienem dodać fragment w Clojure, aby zebrać więcej głosów negatywnych :) ...
Alex Punnen
1
Jak działa składnia? Zgaduję, że Groovy to inny język programowania ...? Przepraszam. Nic nie wiem o Groovy.
HyperNeutrino
-4

Kiedy używasz intellij, spójrz również na:

Plik -> Struktura projektu -> Projekt

Plik -> Struktura projektu -> Moduły

Jeśli masz wiele modułów, upewnij się, że ustawiłeś właściwy poziom języka na karcie modułu.

botenvouwer
źródło
1
Nie jestem pewien, w jaki sposób Twoja odpowiedź jest istotna dla pytania. Zapytał, dlaczego instrukcje przełączania ciągów, takie jak następujące, nie są dostępne: String mystring = "something"; switch (mystring) {case „coś” sysout („dotarłem tutaj”); . . }
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; 
         } 
    } 

} 
Issac Balaji
źródło
8
OP nie pyta, jak włączyć ciąg. Pyta, dlaczego nie jest w stanie, z powodu ograniczeń składni przed JDK7.
HyperNeutrino,