通常的NullPointerException异常其实是很好判断,一定是有某个对象是null导致的。但是java中数字都有自动装箱和拆箱功能,它简化了我们的代码,隐藏了其背后的执行步骤,导致程序抛出异常时我们会有意识的忽略了数字类型也可能会抛NullPointerException。 其实自动装箱和拆箱在字面上很容易使人对其理解的不准确或者不完整,大家都知道装箱就是从基础类型自动转换成对应的对象类型,拆箱就是从对象类型自动转换成基础类型;字面上很好理解,但是背后到底是如何实现的,其实有一种道不清说不明的感觉。自动装箱是不会抛异常的,但是自动拆箱则有可能会,所以本文从三种代码场景,以字节码的角度,还原自动拆箱背后的逻辑。
场景一:赋值时自动拆箱抛NullPointerException
1 2 3 4 5 6 |
public class Test { public static void main(String[] args){ Long a = null; long b = a; } } |
上面的代码在把变量a赋值给b时就会抛出NullPointerException,使用javap Test.class命令查看其对应的字节码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: aconst_null 1: astore_1 2: aload_1 3: invokevirtual #2 // Method java/lang/Long.longValue:()J 6: lstore_2 7: return } |
可以看到main方法行号为3的字节码调用了Long.longValue()方法,行号6的字节码才是把变量a赋值给b,显然由于a是null,所以调用longValue()方法自然会抛空指针了。
场景二:方法传参时自动拆箱抛NullPointerException
1 2 3 4 5 6 7 8 9 10 |
public class Test { public static void main(String[] args){ Long a = null; increment(a); } public static long increment(long n){ return n++; } } |
上面把变量a传参给方法increment时就会抛NullPointerException,还是用javap Test.class查看其字节码如下:
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 |
public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: aconst_null 1: astore_1 2: aload_1 3: invokevirtual #2 // Method java/lang/Long.longValue:()J 6: invokestatic #3 // Method increment:(J)J 9: pop2 10: return public static long increment(long); Code: 0: lload_0 1: dup2 2: lconst_1 3: ladd 4: lstore_0 5: lreturn } |
可以看到main方法行号为3的字节码调用了Long.longValue()方法,行号6的字节码才是真正的执行increment,显然由于a是null,所以调用longValue()方法也会抛空指针了。
场景三:用于大小比较时拆箱抛NullPointerException
1 2 3 4 5 6 7 8 |
public class Test { public static void main(String[] args){ Long n1 = null; long n2 = 1L; if(n1 == n2){ } } } |
同样用javap Test.class查看字节码内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: aconst_null 1: astore_1 2: lconst_1 3: lstore_2 4: aload_1 5: invokevirtual #2 // Method java/lang/Long.longValue:()J 8: lload_2 9: lcmp 10: ifne 13 13: return } |
同样的在行号8的字节码通过Long.longValue()方法把变量n1从Long转换成了long,然后行号8为加载变量n2,行号9才是比较两个long(注意不是比较Long)。
上面提到的字节码中的行号其实是字节码指令占用空间的偏移量,为了便于理解,暂以行号来理解
总结
拆箱的过程其实是调用了对应的XXX.xxxValue()方法,比如:Integer.intValue(), Long.longValue(), Short.shortValue(), Byte.byteValue(), Float.floatValue(), Double.doubleValue(),如果这个对象是null,那么就会抛出空指针异常。