Switch

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 elseową.

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).

Średnia ocen. 0 / 5. Liczba głosów. 0

Brak ocen.