learn and grow up

基础向-从三元运算符和字节码看java包装类的坑

字数统计: 1.3k阅读时长: 6 min
2021/02/13 Share

写在前面

​ 之前经常看资料说不推荐滥用三元运算,刚好之前研究了字节码,那么我从字节码的角度看看三元运算为什么会有坑

正文

​ 直接上示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class HelloWord {
public static void main(String[] args) throws InterruptedException {

Integer a = 1;
Integer b = 2;
Integer c = null;
Integer e = a;
Boolean flag = false;
Integer result = (flag ? a * b : c);

}
}

运行后抛出NPE异常,那么为什么会出现NPE呢?我们直接用javap命令看看编译后的字节码,就可以看出原因来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//解释后的部分字节码如下:
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=7, args_size=1
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1
5: iconst_2
6: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: astore_2
10: aconst_null
11: astore_3
12: aload_1
13: astore 4
15: iconst_0
16: invokestatic #3 // Method java/lang/Boolean.valueOf:(Z)Ljava/lang/Boolean;
19: astore 5
//上面都是变量的声明定义和初始化
//下面就是三元表达式开始

21: aload 5
23: invokevirtual #4 // Method java/lang/Boolean.booleanValue:()Z
//加载布尔型变量,并计算出booleanValue
//ifeq 也就是栈顶booleanValue的值等于0跳转到41,
26: ifeq 41
29: aload_1
30: invokevirtual #5 // Method java/lang/Integer.intValue:()I
33: aload_2
34: invokevirtual #5 // Method java/lang/Integer.intValue:()I
37: imul
38: goto 45
//加载本地变量的第三个变量,也就是null
41: aload_3
//调用intValue,就会NPE
42: invokevirtual #5 // Method java/lang/Integer.intValue:()I
45: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
48: astore 6
50: return

可以看到这里面会将Integer拆箱再装箱,所以才会报NPE,至于为什么会拆箱和装箱,是因为这里有a*b运算,那么整个三元运算符就会被编译器自动拆箱,如果把三元运算改成 Integer result = (flag ? a : c); 那么是不会出现NPE的,也就证明不会被拆箱

拓展

在《深入理解jvm虚拟机》书中也提到过自动拆箱和装箱,还留了一个题目让读者自己思考,那么我们也可以从字节码的角度和java源码角度直接看出答案而不需要运行程序

​ 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d);
System.out.println(e == f);
System.out.println(c == (a + b));
System.out.println(c.equals(a + b));
System.out.println(g == (a + b));
System.out.println(g.equals(a + b));

字节码与答案(见注释)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//解释后的部分字节码如下:
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=8, args_size=1
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1
5: iconst_2
6: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: astore_2
10: iconst_3
11: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: astore_3
15: iconst_3
16: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: astore 4
21: sipush 321
24: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
27: astore 5
29: sipush 321
32: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
35: astore 6
37: ldc2_w #3 // long 3l
40: invokestatic #5 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
43: astore 7
//上面都是变量的声明定义和初始化
//下面是判断
45: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
48: aload_3
49: aload 4
//1、对比本地变量3和4,通过Integer源码可以得到在value在-128和127之间的时候,从缓存取Integer对象,这里也就是c、d是同一个对象,所以这里是true
51: if_acmpne 58
54: iconst_1
55: goto 59
58: iconst_0
59: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
62: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
65: aload 5
67: aload 6
//2、结合1可知,false
69: if_acmpne 76
72: iconst_1
73: goto 77
76: iconst_0
77: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
80: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
83: aload_3
84: invokevirtual #8 // Method java/lang/Integer.intValue:()I
87: aload_1
88: invokevirtual #8 // Method java/lang/Integer.intValue:()I
91: aload_2
92: invokevirtual #8 // Method java/lang/Integer.intValue:()I
95: iadd
//3、可以看到都被拆箱为int,所以==结果是true
96: if_icmpne 103
99: iconst_1
100: goto 104
103: iconst_0
104: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
107: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
110: aload_3
111: aload_1
112: invokevirtual #8 // Method java/lang/Integer.intValue:()I
115: aload_2
116: invokevirtual #8 // Method java/lang/Integer.intValue:()I
119: iadd
120: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
//4、Integer.equals源码内同Integer也是拆箱后==,所以是true
123: invokevirtual #9 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
126: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
129: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
132: aload 7
134: invokevirtual #10 // Method java/lang/Long.longValue:()J
137: aload_1
138: invokevirtual #8 // Method java/lang/Integer.intValue:()I
141: aload_2
142: invokevirtual #8 // Method java/lang/Integer.intValue:()I
145: iadd
//拆箱后add,然后再将int转为long
146: i2l
//对比
147: lcmp
//5、可以看到都被拆箱,int转为long在对比,所以==结果是true
148: ifne 155
151: iconst_1
152: goto 156
155: iconst_0
156: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
159: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
162: aload 7
164: aload_1
165: invokevirtual #8 // Method java/lang/Integer.intValue:()I
168: aload_2
169: invokevirtual #8 // Method java/lang/Integer.intValue:()I
172: iadd
173: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
//6、Long.equals源码内同Long才是拆箱后==,否则类型对比,所以是false
176: invokevirtual #11 // Method java/lang/Long.equals:(Ljava/lang/Object;)Z
179: invokevirtual #7 // Method java/io/PrintStream.println:(Z)V
182: return

所以在日常工作中遇到包装类型的是需要一再仔细和认真,警惕字段拆装箱的陷阱。

CATALOG
  1. 1. 写在前面
  2. 2. 正文
    1. 2.1. 拓展