Pierwsze, co może kojarzyć się ze switch-case
to szereg następujących po sobie bloków if else
. Z pewnością taki blok switch case
jest bardziej czytelny aniżeli szereg if else
. Jednak każdy zna jakąś sytuację, w której jakiś znajomy w pracy zapomniał dodać break
na koniec bloku case
, co prowadziło do błędów biznesowych, technicznych lub błędów bezpieczeństwa. A skoro ten break
jest przeważnie konieczny, to nie do końca pasuje do teorii o ciągu if else
ów. Jaka jest prawda o switch
?
Code
Ok, czas na trochę kodu. Zacznijmy od prostej metody, która w zależności od argumentu zwraca różne wartości. Zaimplementowana będzie dwukrotnie – najpierw za pomocą switch
, następnie z użyciem if
ów.
public int switchInt9(CountToNine countToNine) { int i = countToNine.i; switch (i) { case 0: return 0; case 1: return 8; case 2: return 16; case 3: return 24; case 4: return 32; case 5: return 40; case 6: return 48; case 7: return 56; default: return 64; } } public int ifInt9(CountToNine countToNine) { int i = countToNine.i; if (i == 0) { return 0; } else if (i == 1) { return 8; } else if (i == 2) { return 16; } else if (i == 3) { return 24; } else if (i == 4) { return 32; } else if (i == 5) { return 40; } else if (i == 6) { return 48; } else if (i == 7) { return 56; } else { return 64; } }
Stworzyłem też analogiczne metody z 33 wpisami zamiast 9.
Po skompilowaniu takich metod, a następnie zdekompilowaniu z użyciem javap -v
widzimy obydwie metody. Pierwsza z użyciem switch
wygląda tak:
public int switchInt9(dev.jgardo.jvm.miscellaneous.switches.SwitchBenchmark$CountToNine); descriptor: (Ldev/jgardo/jvm/miscellaneous/switches/SwitchBenchmark$CountToNine;)I flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=3, args_size=2 0: aload_1 1: invokestatic #22 // Method dev/jgardo/jvm/miscellaneous/switches/SwitchBenchmark$CountToNine.access$000:(Ldev/jgardo/jvm/miscellaneous/switches/SwitchBenchmark$CountToNine;)I 4: istore_2 5: iload_2 6: tableswitch { // 0 to 7 0: 52 1: 54 2: 57 3: 60 4: 63 5: 66 6: 69 7: 72 default: 75 } 52: iconst_0 53: ireturn 54: bipush 8 56: ireturn 57: bipush 16 59: ireturn 60: bipush 24 62: ireturn 63: bipush 32 65: ireturn 66: bipush 40 68: ireturn 69: bipush 48 71: ireturn 72: bipush 56 74: ireturn 75: bipush 64 77: ireturn
W tym przypadku widzimy instrukcję kodu bajtowego tableswitch
z pożądanymi wartościami podawanymi przy case
i numerem instrukcji do której ma „skoczyć” jeśli wartość się zgadza. (o tableswitch
więcej poniżej)
Dla if
ów bytecode wygląda następująco:
public int ifInt9(dev.jgardo.jvm.miscellaneous.switches.SwitchBenchmark$CountToNine); descriptor: (Ldev/jgardo/jvm/miscellaneous/switches/SwitchBenchmark$CountToNine;)I flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=3, args_size=2 0: aload_1 1: invokestatic #22 // Method dev/jgardo/jvm/miscellaneous/switches/SwitchBenchmark$CountToNine.access$000:(Ldev/jgardo/jvm/miscellaneous/switches/SwitchBenchmark$CountToNine;)I 4: istore_2 5: iload_2 6: ifne 11 9: iconst_0 10: ireturn 11: iload_2 12: iconst_1 13: if_icmpne 19 16: bipush 8 18: ireturn 19: iload_2 20: iconst_2 21: if_icmpne 27 24: bipush 16 26: ireturn 27: iload_2 28: iconst_3 29: if_icmpne 35 32: bipush 24 34: ireturn 35: iload_2 36: iconst_4 37: if_icmpne 43 40: bipush 32 42: ireturn 43: iload_2 44: iconst_5 45: if_icmpne 51 48: bipush 40 50: ireturn 51: iload_2 52: bipush 6 54: if_icmpne 60 57: bipush 48 59: ireturn 60: iload_2 61: bipush 7 63: if_icmpne 69 66: bipush 56 68: ireturn 69: bipush 64 71: ireturn
W przypadku ciągu if else
ów widzimy… ciąg if else
ów… Czyli na poziomie bytecode’u switch
nie jest ukrytą opcją if else
ową.
Trochę teorii
Otóż w zamyśle do obsługi słowa kluczowego switch
stworzono specjalnie dwie instrukcje bytecodu – tableswitch
oraz lookupswitch
. Zamysł był prosty: zamiast wielokrotnie porównywać z coraz innymi wartościami, na etapie kompilacji stworzymy tablicę par „wartość-adres skoku do instrukcji”. Następnie wystarczyłoby poszukać odpowiedniej wartości w tablicy i skoczyć do tej instrukcji, którą wskazuje.
Dla tableswitch
wyszukiwanie jest proste – wystarczy spojrzeć pod index tablicy, której wartości szukamy. Jeśli szukamy wartości 5, to skaczemy do tej instrukcji, którą wskazuje tablica pod indeksem 5. Wówczas czas obliczenia miejsca kolejnej instrukcji jest stały tzn. O(1)
.