在Java中,If与Switch哪个效率更高?

先说结论: 在Java中, 面对多条件判断的情况, Switch的效率比If更高. 但是If能进行更加复杂的条件判断, 使用场景比Switch更加丰富.

想要探究为什么Switch效率更高, 我们可以从Java编译后的字节码文件入手.

If-Else

若要对数字1到5进行判断, 输出对应的数字, 如果我们采用if-else实现, 代码如下:

public static void ifTest(int n) {
    if (n == 1) {
        System.out.println("n == 1");
    } else if (n == 2) {
        System.out.println("n == 2");
    } else if (n == 3) {
        System.out.println("n == 3");
    } else if (n == 4) {
        System.out.println("n == 4");
    } else if (n == 5) {
        System.out.println("n == 5");
    } else {
        System.out.println("default");
    }
}

编译后的字节码如下:

 0 iload_0
 1 iconst_1
 2 if_icmpne 16 (+14)
 5 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
 8 ldc #13 <n == 1>
10 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
13 goto 88 (+75)
16 iload_0
17 iconst_2
18 if_icmpne 32 (+14)
21 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
24 ldc #21 <n == 2>
26 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
29 goto 88 (+59)
32 iload_0
33 iconst_3
34 if_icmpne 48 (+14)
37 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
40 ldc #23 <n == 3>
42 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
45 goto 88 (+43)
48 iload_0
49 iconst_4
50 if_icmpne 64 (+14)
53 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
56 ldc #25 <n == 4>
58 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
61 goto 88 (+27)
64 iload_0
65 iconst_5
66 if_icmpne 80 (+14)
69 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
72 ldc #27 <n == 5>
74 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
77 goto 88 (+11)
80 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
83 ldc #29 <n > 10>
85 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
88 return

完整的字节码太长, 但是if的五次判断除了判断的内容不一样, 其他都是基本一致的, 所以我们只截取分析第一次判断n是否为1的部分即可.

 0 iload_0 // 将局部变量表中索引为0的变量(参数n)压入操作数栈
 1 iconst_1 // 将常量1压入操作数栈
 2 if_icmpne 16 (+14) // 将操作数栈顶的两者进行比较, 若n不等于1, 则跳转到字节码行号为16的地方继续执行(下一个if判断)
 5 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;> // 获取System.out静态变量
 8 ldc #13 <n == 1> // 从常量池中加载符号引用为#13的常量即字符串: n == 1
10 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V> // 调用System.out的println方法
13 goto 88 (+75) // 跳转到字节码行号为88的地方继续执行(return)

从上述对字节码的分析中我们可以发现, 在最差的情况下, 要将变量加载到操作数栈中五次, 并进行五次判断.

Switch

tableswitch指令

case顺序的情况

下面我们来看看Switch是怎么做的. 相应的Java代码如下:

public static void switchTest(int n) {
    switch (n) {
        case 1:
            System.out.println("n == 1");
            break;
        case 2:
            System.out.println("n == 2");
            break;
        case 3:
            System.out.println("n == 3");
            break;
        case 4:
            System.out.println("n == 4");
            break;
        case 5:
            System.out.println("n == 5");
            break;
        default:
            System.out.println("default");
            break;
    }
}

编译后的字节码如下:

 0 iload_0
 1 tableswitch 1 to 5
	1:  36 (+35)
	2:  47 (+46)
	3:  58 (+57)
	4:  69 (+68)
	5:  80 (+79)
	default:  91 (+90)
36 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
39 ldc #13 <n == 1>
41 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
44 goto 99 (+55)
47 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
50 ldc #21 <n == 2>
52 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
55 goto 99 (+44)
58 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
61 ldc #23 <n == 3>
63 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
66 goto 99 (+33)
69 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
72 ldc #25 <n == 4>
74 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
77 goto 99 (+22)
80 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
83 ldc #27 <n == 5>
85 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
88 goto 99 (+11)
91 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
94 ldc #29 <n > 10>
96 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
99 return

第一眼看上去, 似乎多了一条奇怪的指令tableswitch. 实际上这条指令会为我们维护一个表结构. 在表中保存了switch case中的上下限. 对于在上下限范围内的case, 将与下限相减, 得到一个偏移量, 并将这个偏移量映射到相应的字节码行号上. 反应到字节码中, 就是第1行到第35行的位置.

 1 tableswitch 1 to 5 // 这里的 1 to 5 即是表结构的上下限, 若输入的case并不在这个范围内(比如6), 则会直接跳转到default条件对应的字节码行号执行. 
	1:  36 (+35) // 某个case下, 需要跳转执行的字节码行号
	2:  47 (+46)
	3:  58 (+57)
	4:  69 (+68)
	5:  80 (+79)
	default:  91 (+90)

看文字总是干巴巴的, 所以还是看图比较清晰.

tableswitch指令

通过对tableswitch的说明, 可以发现, 使用switch只需要将局部变量n压入操作数栈一次, 并且也无需一个个去将n与case进行比较, 而是直接计算其在表中的偏移量就可以得知跳转的字节码行号. 因此, switch的速度要比if-else快很多.

case乱序的情况

要使用偏移量, 那么case必然是要顺序的. 若我们将case改成乱序, 按照52341的顺序, 那么还会使用tableswitch吗? 答案是肯定的. 修改后的Java代码如下:

public static void switchTest(int n) {
    switch (n) {
        case 5:
            System.out.println("n == 5");
            break;
        case 2:
            System.out.println("n == 2");
            break;
        case 3:
            System.out.println("n == 3");
            break;
        case 4:
            System.out.println("n == 4");
            break;
        case 1:
            System.out.println("n == 1");
            break;
        default:
            System.out.println("default");
            break;
    }
}

生成的字节码如下:

 0 iload_0
 1 tableswitch 1 to 5
	1:  80 (+79)
	2:  47 (+46)
	3:  58 (+57)
	4:  69 (+68)
	5:  36 (+35)
	default:  91 (+90)
36 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
39 ldc #27 <n == 5>
41 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
44 goto 99 (+55)
47 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
50 ldc #21 <n == 2>
52 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
55 goto 99 (+44)
58 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
61 ldc #23 <n == 3>
63 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
66 goto 99 (+33)
69 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
72 ldc #25 <n == 4>
74 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
77 goto 99 (+22)
80 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
83 ldc #13 <n == 1>
85 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
88 goto 99 (+11)
91 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
94 ldc #29 <default>
96 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
99 return

这时候, 我们发现即使写代码的时候case是乱序的, 但是编译成字节码后, tableswitch仍然是有序的. 只不过按照书写顺序, case 5的情况跳转的字节码行号最小, case 1的情况跳转的字节码行号最大.

case不连续的情况

上面的情况都是case连续的, 如果case并不连续, 例如: 1235, 阁下又该如何应对呢? Java代码如下:

public static void switchTest(int n) {
    switch (n) {
        case 1:
            System.out.println("n == 1");
            break;
        case 2:
            System.out.println("n == 2");
            break;
        case 3:
            System.out.println("n == 3");
            break;
        //case 4:
        //    System.out.println("n == 4");
        //    break;
        case 5:
            System.out.println("n == 5");
            break;
        default:
            System.out.println("default");
            break;
    }
}

编译所得字节码如下:

 0 iload_0
 1 tableswitch 1 to 5
	1:  36 (+35)
	2:  47 (+46)
	3:  58 (+57)
	4:  80 (+79)
	5:  69 (+68)
	default:  80 (+79)
36 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
39 ldc #13 <n == 1>
41 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
44 goto 88 (+44)
47 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
50 ldc #21 <n == 2>
52 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
55 goto 88 (+33)
58 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
61 ldc #23 <n == 3>
63 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
66 goto 88 (+22)
69 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
72 ldc #27 <n == 5>
74 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
77 goto 88 (+11)
80 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
83 ldc #29 <default>
85 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
88 return

我们可以只看其中的关键部分:

 1 tableswitch 1 to 5
	1:  36 (+35)
	2:  47 (+46)
	3:  58 (+57)
	4:  80 (+79)
	5:  69 (+68)
	default:  80 (+79)

哦? 我们注释掉的case 4的情况, 在字节码中被补全了! 只是case 4跳转的字节码行号和case default一样, 与我们不写case 4是等价的. 这么做是为了保证在case不连续的情况下仍然可以通过偏移量快速得知跳转字节码行号.

lookupswitch指令

现在我们来考虑另一种新的情况. Java代码如下:

public static void switchTest(int n) {
    switch(n) {
        case 100:
            System.out.println("n == 100");
            break;
        case 200:
            System.out.println("n == 200");
            break;
        case 300:
            System.out.println("n == 300");
            break;
        default:
            System.out.println("default");
            break;
    }
}

这时候再来看编译后的字节码文件:

  0 iload_0
 1 lookupswitch 3
	100:  36 (+35)
	200:  47 (+46)
	300:  58 (+57)
	default:  69 (+68)
36 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
39 ldc #31 <n == 100>
41 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
44 goto 77 (+33)
47 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
50 ldc #33 <n == 200>
52 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
55 goto 77 (+22)
58 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
61 ldc #35 <n == 300>
63 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
66 goto 77 (+11)
69 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
72 ldc #29 <default>
74 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
77 return

可以发现, 这时候使用的是lookupswitch指令而不是tableswitch了. 产生这种结果的原因也很容易理解. 在这个例子中, case的跨度很大, 如果和tableswitch指令一样将偏移量作为表中的索引, 那么将浪费大量空间来保存这个表(在这个例子中是从100到300共两百项).

实际上, 在JVM中使用二分查找来尽可能进行快速查找. 若case乱序, 首先需要将case进行排序(与使用tableswitch的情况一致). 随后将排序完成后的case放入表结构中. 本例具体查找流程见下图:

lookupswitch指令

当Case是枚举类时

从JDK7开始, Java开始将Enum作为switch语句中的case标签. 但在The Java Virtual Machine Specification中却告诉我们, JVM中的tableswitch和lookupswitch指令只能支持int数据类型. 因此, 在处理枚举类时一定做了什么与处理int型数据不同的特殊操作. 测试Java代码如下:

public static void switchTest(Color c) {
    switch (c) {
        case RED:
            System.out.println("RED");
            break;
        case BLUE:
            System.out.println("BLUE");
            break;
        case YELLOW:
            System.out.println("YELLOW");
            break;
        default:
            System.out.println("default");
            break;
    }
}

enum Color {
    RED, BLUE, YELLOW;
}

编译后的字节码如下:

 0 getstatic #31 <org/example/chapter5/LookupSwitchAndTablesSwitch$1.$SwitchMap$org$example$chapter5$Color : [I>
 3 aload_0
 4 invokevirtual #37 <org/example/chapter5/Color.ordinal : ()I>
 7 iaload
 8 tableswitch 1 to 3
	1:  36 (+28)
	2:  47 (+39)
	3:  58 (+50)
	default:  69 (+61)
36 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
39 ldc #43 <RED>
41 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
44 goto 77 (+33)
47 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
50 ldc #45 <BLUE>
52 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
55 goto 77 (+22)
58 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
61 ldc #47 <YELLOW>
63 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
66 goto 77 (+11)
69 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
72 ldc #29 <default>
74 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
77 return

实际上, 当使用Enum作为case标签时需要维护两个表格. 在第一个表格中, 索引为Enum对象的序号(Enum.ordinal()方法的返回值), 而值为连续整数. 在第二张表格中, 与先前的tableswitch一样, 会保存第一张表格中的lo值和hi值, 并通过偏移量得到字节码行号. 本例查找流程如下图:

CaseEnum

当Case是String字符串时

与Enum相同, 在JDK7中, 字符串也可以作为Case标签使用了. 相关Java代码如下:

public static void stringCaseTest(String s) {
    switch (s) {
        case "Aa":
            System.out.println("Aa");
            break;
        case "aa":
            System.out.println("aa");
            break;
        case "BB":
            System.out.println("BB");
            break;
        default:
            System.out.println("default");
    }
}

编译后的字节码如下:

  0 aload_0
  1 astore_1
  2 iconst_m1
  3 istore_2
  4 aload_1
  5 invokevirtual #49 <java/lang/String.hashCode : ()I>
  8 lookupswitch 2
	2112:  36 (+28)
	3104:  64 (+56)
	default:  75 (+67)
 36 aload_1
 37 ldc #54 <BB>
 39 invokevirtual #56 <java/lang/String.equals : (Ljava/lang/Object;)Z>
 42 ifeq 50 (+8)
 45 iconst_2
 46 istore_2
 47 goto 75 (+28)
 50 aload_1
 51 ldc #60 <Aa>
 53 invokevirtual #56 <java/lang/String.equals : (Ljava/lang/Object;)Z>
 56 ifeq 75 (+19)
 59 iconst_0
 60 istore_2
 61 goto 75 (+14)
 64 aload_1
 65 ldc #62 <aa>
 67 invokevirtual #56 <java/lang/String.equals : (Ljava/lang/Object;)Z>
 70 ifeq 75 (+5)
 73 iconst_1
 74 istore_2
 75 iload_2
 76 tableswitch 0 to 2
	0:  104 (+28)
	1:  115 (+39)
	2:  126 (+50)
	default:  137 (+61)
104 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
107 ldc #60 <Aa>
109 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
112 goto 145 (+33)
115 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
118 ldc #62 <aa>
120 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
123 goto 145 (+22)
126 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
129 ldc #54 <BB>
131 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
134 goto 145 (+11)
137 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
140 ldc #29 <default>
142 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
145 return

由于tableswitch和lookupswitch都只能处理int型数据的情况, 而要将String类型映射为int型数据, 很容易想到字符串的Hashcode. 实际上Java也是这么做的, 从第5行字节码中可以看到, 我们调用了s参数的hashCode方法. 由于字符串的Hash值并不是连续的, 所以采用了lookupswitch指令对Hash值进行二分查找. 但是我们知道, 仅仅比较字符串的Hash值相同是不足以判断两个字符串是否相等的(拥有相同Hash值的字符串不一定相等, 但拥有不同Hash值的字符串一定不同. 比如上述Java代码中的AaBB, 他们的Hash值就完全相同), 因此要进行标签精确的匹配, 我们就必须在通过hash值进行筛选后, 再对拥有相同Hash值的字符串进行equals比较. 比较完成后, 根据比较结果, 可以为一个局部变量赋连续的整数, 并将这个局部变量作为tableswitch指令查询的索引, 快速获得跳转的字节码行号. 上述过程从编译后的Class文件反编译回的java代码中也可以很明显地看到.

// 将编译后的Class文件进行反编译后得到的java代码
public static void stringCaseTest(String s) {
    byte var2 = -1;
    switch (s) {
        case "BB":
            var2 = 2;

            if (s.equals("Aa")) {
                var2 = 0;
            }
            break;
        case "aa":
            var2 = 1;
    }

    switch (var2) {
        case 0:
            System.out.println("Aa");
            break;
        case 1:
            System.out.println("aa");
            break;
        case 2:
            System.out.println("BB");
            break;
        default:
            System.out.println("default");
    }

}

本例具体查找流程见下图:

CaseString

相关题目

最后再来几道与switch相关的题目.

public class SwitchTest {
    public static void main(String[] args) {
      //当default在中间时,且看输出是什么?
        int a = 1;
        switch (a) {
            case 2:
                System.out.println("print 2");
            case 1:
                System.out.println("print 1");
            default:
                System.out.println("first default print");
            case 3:
                System.out.println("print 3");
        }
      
      //当switch括号内的变量为String类型的外部参数时,且看输出是什么?
        String param = null;
        switch (param) {
            case "param":
                System.out.println("print param");
                break;
            case "String":
                System.out.println("print String");
                break;
            case "null":
                System.out.println("print null");
                break;
            default:
                System.out.println("second default print");
        }
    }
}

本文部分参考文章:

【17】Java深入了解 if 和 switch 语句?switch 的底层数据结构?switch 是如何匹配枚举和字符串的? - 知乎 (zhihu.com)

why哥被阿里一道基础面试题给干懵了,一气之下写出万字长文。 - why技术 - 博客园 (cnblogs.com)

2023.9.25备注: 在最新发布的JDK21 LTS中, 正式引入了增强模式匹配, 这部分内容待后续有时间了再看看(可能)...

文章作者: Serendipity
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 闲人亭
Java杂谈 Java JVM
喜欢就支持一下吧