Dlaczego instrukcja String switch nie obsługuje wielkości liter o wartości null?

125

Zastanawiam się tylko, dlaczego instrukcja Java 7 switchnie obsługuje nullprzypadku i zamiast tego wyrzuca NullPointerException? Zobacz skomentowaną linię poniżej (przykład zaczerpnięty z artykułu Java Tutorialsswitch ):

{
    String month = null;
    switch (month) {
        case "january":
            monthNumber = 1;
            break;
        case "february":
            monthNumber = 2;
            break;
        case "march":
            monthNumber = 3;
            break;
        //case null:
        default: 
            monthNumber = 0;
            break;
    }

    return monthNumber;
}

Pozwoliłoby to uniknąć ifwarunku zerowego sprawdzania przed każdym switchużyciem.

Prashant Bhate
źródło
12
Brak rozstrzygającej odpowiedzi na to pytanie, ponieważ nie jesteśmy ludźmi, którzy stworzyli język. Wszystkie odpowiedzi będą czystym przypuszczeniem.
asteri
2
Próba włączenia nullspowoduje wyjątek. Wykonać ifczek null, a następnie przejść do switchzestawienia.
gparyani
28
Z JLS : W ocenie projektantów języka programowania Java, [rzucenie a, NullPointerExceptionjeśli wyrażenie ma wartość nullw czasie wykonywania] jest lepszym wynikiem niż ciche pomijanie całej instrukcji switch lub wybieranie wykonania instrukcji (jeśli istnieją) po etykieta domyślna (jeśli istnieje).
gparyani
3
@gparyani: Niech to będzie odpowiedź. To brzmi bardzo oficjalnie i definitywnie.
Thilo
7
@JeffGohlke: "Nie ma sposobu, aby odpowiedzieć na pytanie dlaczego, chyba że jesteś osobą, która podjęła decyzję." ... cóż, komentarz gparyani udowadnia, że ​​jest inaczej
user541686

Odpowiedzi:

145

Jak wskazuje damryfbfnetsi w komentarzach, JLS §14.11 ma następującą uwagę:

Zakaz używania nulljako etykiety przełącznika uniemożliwia pisanie kodu, którego nigdy nie można wykonać. Jeśli switchwyrażenie jest typu referencyjnego, to znaczy Stringtypu pierwotnego w pudełku lub typu wyliczeniowego, wystąpi błąd w czasie wykonywania, jeśli wartość wyrażenia zostanie obliczona nullw czasie wykonywania. W ocenie projektantów języka programowania Java jest to lepszy wynik niż ciche pomijanie całej switchinstrukcji lub wybieranie wykonania instrukcji (jeśli istnieją) po defaultetykiecie (jeśli istnieje).

(podkreślenie moje)

Chociaż ostatnie zdanie pomija możliwość użycia case null:, wydaje się rozsądne i daje wgląd w intencje projektantów języka.

Jeśli wolimy spojrzeć na szczegóły implementacji, ten post na blogu Christiana Hujera zawiera wnikliwe spekulacje na temat tego, dlaczego nullnie jest to dozwolone w przełącznikach (chociaż koncentruje się na enumprzełączniku, a nie na Stringprzełączniku):

Pod maską switchinstrukcja zwykle kompiluje się do kodu bajtowego przełącznika tabeli. A argumentem „fizycznym” switchtak samo jak jego przypadki są ints. Wartość int do włączenia jest określana przez wywołanie metody Enum.ordinal(). Liczby porządkowe [...] zaczynają się od zera.

Oznacza to, że mapowanie nullna 0nie byłoby dobrym pomysłem. Przełącznik na pierwszej wartości wyliczenia byłby nie do odróżnienia od null. Być może dobrym pomysłem byłoby rozpoczęcie liczenia liczb porządkowych dla wyliczeń od 1. Jednak nie zostało to tak zdefiniowane i tej definicji nie można zmienić.

Chociaż Stringprzełączniki są implementowane w różny sposób , enumprzełącznik pojawił się jako pierwszy i ustawił precedens dotyczący tego, jak powinno zachowywać się włączenie typu odniesienia, gdy jest to odniesienie null.

Paul Bellora
źródło
1
Dopuszczenie obsługi zerowej jako części a, case null:gdyby został zaimplementowany na wyłączność, byłoby dużym usprawnieniem String. Obecnie wszystkie Stringsprawdzania wymagają sprawdzenia zerowego, jeśli chcemy to zrobić poprawnie, chociaż głównie niejawnie, umieszczając najpierw stałą łańcuchową, jak in "123test".equals(value). Teraz jesteśmy zmuszeni napisać nasze oświadczenie o przełączniku, jak wif (value != null) switch (value) {...
YoYo
1
Re „odwzorowanie wartości null na 0 nie byłoby dobrym pomysłem”: to mało powiedziane, ponieważ wartość „” .hashcode () wynosi 0! Oznaczałoby to, że łańcuch pusty i łańcuch o zerowej długości musiałyby być traktowane identycznie w instrukcji switch, co jest oczywiście nierealne.
skomisa
W przypadku wyliczenia, co powstrzymywało je przed mapowaniem wartości null na -1?
krispy
31

Ogólnie nulljest nieprzyjemny w obsłudze; może bez lepszego języka da się żyć null.

Twój problem może zostać rozwiązany przez

    switch(month==null?"":month)
    {
        ...
        //case "":
        default: 
            monthNumber = 0;

    }
ZhongYu
źródło
Nie jest to dobry pomysł, jeśli monthjest to pusty ciąg: potraktuje to tak samo, jak łańcuch pusty.
gparyani,
13
W wielu przypadkach traktowanie null jako pustego ciągu może być całkowicie rozsądne
Eric Woodruff
23

Nie jest ładny, ale String.valueOf()pozwala na użycie zerowego ciągu w przełączniku. Jeśli znajdzie null, konwertuje go na "null", w przeciwnym razie po prostu zwraca ten sam ciąg, który go przekazałeś. Jeśli nie poradzisz sobie "null"bezpośrednio, przejdzie do default. Jedynym zastrzeżeniem jest to, że nie ma sposobu na rozróżnienie między String "null"a rzeczywistą nullzmienną.

    String month = null;
    switch (String.valueOf(month)) {
        case "january":
            monthNumber = 1;
            break;
        case "february":
            monthNumber = 2;
            break;
        case "march":
            monthNumber = 3;
            break;
        case "null":
            monthNumber = -1;
            break;
        default: 
            monthNumber = 0;
            break;
    }
    return monthNumber;
krispy
źródło
2
Uważam, że zrobienie czegoś takiego w Javie jest antywzorem.
Łukasz Rzeszotarski
2
@ ŁukaszRzeszotarski To właśnie miałem na myśli mówiąc „to nie jest ładne”
krispy
15

To próba odpowiedzi, dlaczego rzuca NullPointerException

Dane wyjściowe poniższego polecenia javap ujawniają, że casejest ono wybierane na podstawie kodu skrótu switchciągu argumentu i dlatego rzuca NPE, gdy .hashCode()jest wywoływane na łańcuchu zerowym.

6: invokevirtual #18                 // Method java/lang/String.hashCode:()I
9: lookupswitch  { // 3
    -1826660246: 44
     -263893086: 56
      103666243: 68
        default: 95
   }

Oznacza to, w oparciu o odpowiedzi na pytanie Czy kod hashCode języka Java może generować tę samą wartość dla różnych ciągów? , choć rzadko, nadal istnieje możliwość dopasowania dwóch przypadków (dwa ciągi znaków z tym samym kodem skrótu) Zobacz poniższy przykład

    int monthNumber;
    String month = args[0];

    switch (month) {
    case "Ea":
        monthNumber = 1;
        break;
    case "FB":
        monthNumber = 2;
        break;
    // case null:
    default:
        monthNumber = 0;
        break;
    }
    System.out.println(monthNumber);

javap dla którego

  10: lookupswitch  { // 1
              2236: 28
           default: 59
      }
  28: aload_3       
  29: ldc           #22                 // String Ea
  31: invokevirtual #24                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
  34: ifne          49
  37: aload_3       
  38: ldc           #28                 // String FB
  40: invokevirtual #24                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
  43: ifne          54
  46: goto          59 //Default

Jak widać, generowany jest tylko jeden przypadek "Ea" , a "FB"jednak z dwóch ifwarunków, aby sprawdzić na mecz z każdym przypadku łańcucha. Bardzo ciekawy i skomplikowany sposób implementacji tej funkcjonalności!

Prashant Bhate
źródło
5
Można to jednak zaimplementować w inny sposób.
Thilo,
2
Powiedziałbym, że to błąd projektowy.
Deer Hunter
6
Zastanawiam się, czy ma to związek z tym, dlaczego niektóre wątpliwe aspekty funkcji skrótu ciągu nie zostały zmienione: generalnie kod nie powinien polegać na hashCodezwracaniu tych samych wartości w różnych uruchomieniach programu, ale ponieważ skróty ciągów są upieczone w plikach wykonywalnych przez kompilator, metoda skrótu napisów kończy się jako część specyfikacji języka.
supercat
5

Krótko mówiąc ... (i miejmy nadzieję, że wystarczająco interesujące !!!)

Enum zostały po raz pierwszy wprowadzone w Javie 1.5 ( wrzesień 2004 ), a błąd żądający zezwolenia na włączenie String został zgłoszony dawno temu ( październik 1995 ). Jeśli spojrzysz na komentarz opublikowany na temat tego błędu w czerwcu 2004 , mówi, że Don't hold your breath. Nothing resembling this is in our plans.wygląda na to, że odroczyli ( zignorowali ) ten błąd i ostatecznie uruchomili Javę 1.5 w tym samym roku, w którym wprowadzili 'wyliczenie' z liczbą porządkową zaczynającą się od 0 i zdecydowali ( brakowało ), aby nie obsługiwać wartości null dla wyliczenia. Później w Javie 1.7 ( lipiec 2011 ) poszli za nim ( wymuszeni) ta sama filozofia co w przypadku String (tj. podczas generowania kodu bajtowego nie przeprowadzono sprawdzenia wartości null przed wywołaniem metody hashcode ()).

Więc myślę, że sprowadza się to do faktu, że wyliczenie pojawiło się jako pierwsze i zostało zaimplementowane z początkiem porządkowym od 0, przez co nie mogli obsługiwać wartości null w bloku przełącznika, a później z String postanowili wymusić tę samą filozofię, tj. Wartość null nie dozwolone w bloku przełączników.

TL; DR Dzięki String mogliby zająć się NPE (spowodowanym próbą wygenerowania hashcode dla null) podczas implementacji kodu java do konwersji kodu bajtowego, ale ostatecznie zdecydowali się tego nie robić.

Ref: TheBUG , JavaVersionHistory , JavaCodeToByteCode , SO

sactiw
źródło
1

Według Java Docs:

Przełącznik działa z pierwotnymi typami danych bajt, short, char i int. Działa również z typami wyliczonymi (omówionymi w typach wyliczeniowych), klasą String i kilkoma klasami specjalnymi, które zawijają pewne typy pierwotne: znak, bajt, krótki i całkowity (omówione w liczbach i ciągach znaków).

Ponieważ nullnie ma typu i nie jest instancją niczego, nie będzie działać z instrukcją switch.

BlackHatSamurai
źródło
4
A jednak nulljest prawidłową wartością dla String, Character, Byte, Short, lub Integerodniesienia.
asteri
0

Odpowiedź jest prosta, jeśli użyjesz przełącznika z typem referencyjnym (takim jak typ pierwotny pudełkowy), błąd czasu wykonania wystąpi, jeśli wyrażenie jest puste, ponieważ rozpakowanie go wyrzuci NPE.

więc case null (co jest nielegalne) i tak nigdy nie mogłoby zostać wykonane;)

amrith
źródło
1
Można to jednak zaimplementować w inny sposób.
Thilo
OK @Thilo, mądrzejsi ode mnie byli zaangażowani w tę implementację. Jeśli znasz inne sposoby, w jakie można to zaimplementować, chciałbym wiedzieć, jakie to są [i jestem pewien, że są inne], więc podziel się ...
amrith
3
Łańcuchy nie są opakowanymi typami pierwotnymi, a NPE nie występuje, ponieważ ktoś próbuje je „rozpakować”.
Thilo
@thilo, inne sposoby realizacji tego to, co?
amrith
3
if (x == null) { // the case: null part }
Thilo,
0

Zgadzam się z wnikliwymi komentarzami (Under the hood…) w https://stackoverflow.com/a/18263594/1053496 w odpowiedzi @Paul Bellora.

Z własnego doświadczenia znalazłem jeszcze jeden powód.

Jeśli „przypadek” może mieć wartość null, co oznacza, że ​​przełącznik (zmienna) jest pusty, to tak długo, jak programista dostarcza pasującą wielkość „null”, możemy argumentować, że jest w porządku. Ale co się stanie, jeśli programista nie poda żadnego pasującego przypadku „zerowego”. Następnie musimy dopasować to do „domyślnego” przypadku, który może nie być tym, co programista zamierzał obsłużyć w domyślnym przypadku. Dlatego dopasowanie wartości „null” do wartości domyślnej może spowodować „zaskakujące zachowanie”. Dlatego rzucenie 'NPE' zmusi programistę do obsługi każdego przypadku jawnie. Uważam, że rzucanie NPE w tym przypadku jest bardzo przemyślane.

nantitv
źródło
0

Użyj klasy Apache StringUtils

String month = null;
switch (StringUtils.trimToEmpty(month)) {
    case "xyz":
        monthNumber=1;  
    break;
    default:
       monthNumber=0;
    break;
}
bhagat
źródło