昨天有人在群里问了一个问题:

System.out.println(String.valueOf(null));

这段代码会抛出 NullPointerException,下面这样却可以正常打印 null:

Object obj = null;
System.out.println(String.valueOf(obj));

看一下字节码或者跟进源码,会发现它们调用了不同的重载方法。第一段调用的是:

public static String valueOf(char[] data) {
    return new String(data);
}

传入的字符数组是 null,创建 String 时就会抛出空指针异常。第二段调用的是:

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

这里显式处理了 null,所以返回字符串 "null"。问题不在 println,而在编译器为同一个方法名选择了不同的重载。

重载为什么选择 char[]

String.valueOf() 在 Java 8 中有这些重载:

static String valueOf(boolean value)
static String valueOf(char value)
static String valueOf(char[] data)
static String valueOf(char[] data, int offset, int count)
static String valueOf(double value)
static String valueOf(float value)
static String valueOf(int value)
static String valueOf(long value)
static String valueOf(Object obj)

方法重载在编译期解析。编译器先根据方法名和参数个数找出候选方法,再按严格调用、宽松调用和可变参数调用几个阶段判断哪些方法适用,最后从适用的方法中选择最具体的一个。

null 不能转换成基本类型,所以 boolean、char、int 等重载都不适用;三个参数的 valueOf(char[], int, int) 参数个数也不匹配。剩下的两个候选方法是:

valueOf(char[] data)
valueOf(Object obj)

null 可以转换成任意引用类型,因此两个方法都适用。因为 char[] 是 Object 的子类型,valueOf(char[]) 比 valueOf(Object) 更具体,最终选中的就是 char[] 版本。

反编译后可以直接看到方法描述符是 ([C)Ljava/lang/String;,其中 [C 表示 char[]:

aconst_null
invokestatic java/lang/String.valueOf:([C)Ljava/lang/String;

如果两个候选参数之间没有继承关系,编译器就无法找出唯一的最具体方法:

static void show(Integer value) {
    System.out.println("Integer");
}

static void show(Long value) {
    System.out.println("Long");
}

static void show(Object value) {
    System.out.println("Object");
}

show(null); // 编译失败:对 show 的引用不明确

Integer 和 Long 都比 Object 具体,但两者互不继承,因此没有唯一答案。

想明确调用 valueOf(Object),最直接的写法是给 null 指定类型:

System.out.println(String.valueOf((Object) null));

下面这段同样输出 null:

System.out.println(String.valueOf((Integer) null));

此时 valueOf(Object) 可以通过引用类型的拓宽转换匹配;valueOf(int) 还需要拆箱。编译器在严格调用阶段已经找到 Object 版本,不会再进入需要拆箱的宽松调用阶段。

重载选择的详细规则可以参考 JLS 15.12.2.5。

null 到底是什么

null 不是一个对象。按照 Java 语言规范,表达式 null 有一个特殊的 null 类型,这个类型没有名字,因此不能声明 null 类型的变量,也不能把其他值强制转换成 null 类型。null 引用是这个类型唯一可能的值,并且可以赋值或转换成任意引用类型。

这也解释了为什么下面的表达式始终为 false:

System.out.println(null instanceof Object); // false

变量本身仍然有声明类型,例如 Object obj = null 中 obj 的类型是 Object,只是它当前保存的值是 null 引用。编译器会根据变量的声明类型参与重载解析,所以它和直接传入 null 字面量的结果不同。

null 引用调用实例方法时会抛出 NullPointerException:

String text = null;
System.out.println(text.length());

但通过 null 引用调用静态方法却可以正常执行:

public class Demo {
    static void show(Object value) {
        System.out.println(value);
    }

    public static void main(String[] args) {
        Demo demo = null;
        demo.show("hello");
    }
}

输出结果是:

hello

静态方法在编译期就确定为 Demo.show(Object),调用时不需要目标对象。demo 这个表达式仍会被求值,但结果会被丢弃,不会因为结果是 null 而抛出异常。字节码中的调用指令也是 invokestatic:

aload_1
pop
ldc "hello"
invokestatic Demo.show:(Ljava/lang/Object;)V

虽然这种写法能够运行,但很容易让人误以为调用的是实例方法。静态成员应该通过类名访问:

Demo.show("hello");

null 本身的规则并不复杂,容易混淆的地方通常来自它与重载、拆箱和静态成员调用组合在一起。null 类型可以参考 JLS 4.1,静态方法调用时的目标引用规则可以参考 JLS 15.12.4.1。

最后修改:2017 年 09 月 10 日
如果觉得我的文章对你有用,请随意赞赏