Android中文API
ADC-Android API Android SDK Android Studio
当前位置: 主页 > Android开发文档 >

从一个 NullPointerException 探究 Java 的自动装箱拆箱

时间:2017-08-22 20:22来源:未知 作者:卓一哥 点击:
前天遇到了一个 NullPointerException,触发的代码类似下面这样: public class Test { public static long test ( long value) { return value; } public static void main (String[] args) { Long value = null ; // ... test(value);

前天遇到了一个 NullPointerException,触发的代码类似下面这样:

public class Test {
    public static long test(long value) {
        return value;
    }

    public static void main(String[] args) {
        Long value = null;
        // ...
        test(value);
    }
}

main 方法里的代码实际上相当于调用 test(null);,为什么不直接这样写呢?因为编译不过,会报 错误: 不兼容的类型: <空值>无法转换为long

抛出问题

运行时提示 test(value); 这一行抛出 NullPointerException,但是看着以上代码会有些许困惑:以上代码里一个对象方法都没有调用啊,NullPointerException 从何而来?

原因分析

这时,如果留意到 test 方法接受的参数是 long 类型,而我们传入的是 Long 类型(虽然其实是 null),就会想到这会经历一次从类型 Long 到基本数据类型 long 的自动拆箱过程,那会不会是这个过程中抛出的 NullPointerException 呢?因为以前只知道 Java 为一些基础数据类型与对应的包装器类型之间提供了自动装箱拆箱机制,而并不知这机制的具体实现方法是怎么样的,正好学习一下。

用命令 javap -c Test 将以上代码编译出的 Test.class 文件进行反汇编,可以看到如下输出:

Compiled from "Test.java"
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static long test(long);
    Code:
       0: lload_0
       1: lreturn

  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 test:(J)J
       9: pop2
      10: return
}

从以上字节码及对应的注释可以看出,test(value); 这一行被编译后等同于如下代码:

long primitive = value.longValue();
test(promitive);

相比实际代码,多出的 long primitive = value.longValue(); 这一行看起来就是自动拆箱的过程了,而我们传入的 value 为 null,value.longValue() 会抛出 NullPointerException,一切就解释得通了。用更简洁的代码表达出了更丰富的含义,这就是所谓的语法糖了。

证实猜想

那么我们上面得出的自动拆箱机制的结论是否正确呢?选择一种其它基本数据类型,比如 int,来佐证一下:

public class Test {
    public static void main(String[] args) {
        Integer value = 10;
        int primitive = value;
    }
}

反汇编后对应的字节码:

Compiled from "Test.java"
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: bipush        10
       2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: astore_1
       6: aload_1
       7: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
      10: istore_2
      11: return
}

由以上字节码我们可以印证下文里的知识点了。

自动装箱与拆箱

自动装箱与拆箱是 Java 1.5 引入的新特性,是一种语法糖。

在此之前,我们要创建一个值为 10 的 Integer 对象,只能写作:

Integer value = new Integer(10);

而现在,我们可以更方便地写为:

Integer value = 10;

定义与实现机制

自动装箱,是指从基本数据类型值到其对应的包装类对象的自动转换。比如 Integer value = 10;,是通过调用 Integer.valueOf 方法实现转换的。

自动拆箱,是指从包装类对象到其对应的基本数据类型值的自动转换。比如 int primitive = value;,是通过调用 Integer.intValue 方法实现转换的。

基本数据类型 包装类型 装箱方法 拆箱方法
boolean Boolean Boolean.valueOf(boolean) Boolean.booleanValue()
byte Byte Byte.valueOf(byte) Byte.byteValue()
char Character Character.valueOf(char) Character.charValue()
short Short Short.valueOf(short) Short.shortValue()
int Integer Integer.valueOf(int) Integer.intValue()
long Long Long.valueOf(long)