昨天有人在群里问了一个问题:
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。