布尔值、条件运算符和自动装箱

为什么这个会抛出 NullPointerException

public static void main(String[] args) throws Exception {
Boolean b = true ? returnsNull() : false; // NPE on this line.
System.out.println(b);
}


public static Boolean returnsNull() {
return null;
}

而这个没有

public static void main(String[] args) throws Exception {
Boolean b = true ? null : false;
System.out.println(b); // null
}

解决方案是用 Boolean.FALSE代替 false,以避免 null被解封到 boolean——这是不可能的。但这不是问题所在。问题是 为什么?是否有任何参考 JLS 证实这种行为,特别是第二种情况?

22889 次浏览

台词是:

    Boolean b = true ? returnsNull() : false;

在内部转化为:

    Boolean b = true ? returnsNull().booleanValue() : false;

执行拆箱操作; 因此: null.booleanValue()将产生 NPE

这是使用自动装箱时的主要缺陷之一

编辑: 我相信解除装箱是由于第三个操作符是布尔类型的,如(隐式转换添加) :

   Boolean b = (Boolean) true ? true : false;

来自 Java 语言规范,第15.25节:

  • 如果第二个和第三个 操作数的类型为布尔型,而 另一个的类型是布尔型, 那么条件的类型 表达式是布尔型的。

因此,第一个示例尝试调用 Boolean.booleanValue(),以便按照第一条规则将 Boolean转换为 boolean

在第二种情况下,当第二个操作数不是引用类型时,第一个操作数为 null 类型,因此应用自动装箱转换:

  • 否则,第二次和第三次 操作数的类型为 S1和 S2 设 T1为 运用拳击的结果 转换为 S1,并且让 T2成为 应用装箱而产生的类型 转换为 S2 条件表达式是结果 捕获转换的应用 (5.1.10)至润滑油(T1,T2)(15.12.2.7)。

不同之处在于,returnsNull()方法的显式类型会影响表达式在编译时的静态类型:

E1: `true ? returnsNull() : false` - boolean (auto-unboxing 2nd operand to boolean)


E2: `true ? null : false` - Boolean (autoboxing of 3rd operand to Boolean)

请参阅 Java 语言规范, 15.25条件运算符? : 部分

  • 对于 E1,第二个和第三个操作数的类型分别是 Booleanboolean,因此这个子句适用于:

    如果第二个和第三个操作数的类型为布尔型,而另一个操作数的类型为布尔型,则条件表达式的类型为布尔型。

    因为表达式的类型是 boolean,所以必须将第2个操作数强制为 boolean。编译器将自动取消装箱代码插入到第2个操作数(返回值 returnsNull()) ,使其类型为 boolean。这当然会导致在运行时从 null返回 NPE。

  • 对于 E2,第二个和第三个操作数的类型是 <special null type>(而不是 E1中的 Boolean!)和 boolean,因此没有特定的类型子句适用(go read 'em!) ,所以最后的“否则”子句适用:

    否则,第二个和第三个操作数分别是 S1和 S2类型。让 T1成为将装箱转换应用于 S1所产生的类型,让 T2成为将装箱转换应用于 S2所产生的类型。条件表达式的类型是将捕获转换(5.1.10)应用于 lub (T1,T2)(15.12.2.7)的结果。

    • S1 = = <special null type>(见 4.1)
    • S2 = = boolean
    • T1 = = box (S1) = = <special null type>(请参见 5.1.7中装箱转换列表中的最后一项)
    • T2 = = box (S2) = = ‘ Boolean
    • Lub (T1,T2) = = Boolean

    所以条件表达式的类型是 Boolean,第三个操作数必须强制为 Boolean。编译器插入第3个操作数(false)的自动装箱代码。第二个操作数不需要像 E1那样自动解压缩,所以当返回 null时不需要自动解压缩 NPE。


这个问题需要进行类似的类型分析:

Java 条件运算符? : result type

我们可以从字节码中看到这个问题。在 main 的字节码 3: invokevirtual #3 // Method java/lang/Boolean.booleanValue:()Z的第3行,值为 null 的装箱布尔值,invokevirtual的方法 java.lang.Boolean.booleanValue,它当然会抛出 NPE。

    public static void main(java.lang.String[]) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: invokestatic  #2                  // Method returnsNull:()Ljava/lang/Boolean;
3: invokevirtual #3                  // Method java/lang/Boolean.booleanValue:()Z
6: invokestatic  #4                  // Method java/lang/Boolean.valueOf:(Z)Ljava/lang/Boolean;
9: astore_1
10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
13: aload_1
14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
17: return
LineNumberTable:
line 3: 0
line 4: 10
line 5: 17
Exceptions:
throws java.lang.Exception


public static java.lang.Boolean returnsNull();
descriptor: ()Ljava/lang/Boolean;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: aconst_null
1: areturn
LineNumberTable:
line 8: 0