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


132

为什么会抛出 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
}

解决方案是通过替换false方式Boolean.FALSE来避免null被取消装箱-这boolean是不可能的。但这不是问题。问题是为什么?JLS中是否有任何引用可以证实这种行为,尤其是第二种情况?


28
哇,自动装箱是...的无穷资源...令Java程序员大吃一惊,不是吗?:-)
leonbloy

我有一个类似的问题,令我惊讶的是,它在OpenJDK VM上失败了,但是在HotSpot VM上工作了……写一次,就可以在任何地方运行!
kodu

Answers:


92

区别在于方法的显式类型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,因此第二个操作数必须强制为boolean。编译器将自动拆箱代码插入第二个操作数(返回值returnsNull())以使其为type boolean。当然,这会导致NPE null在运行时返回。

  • 对于E2,第二和第三操作数的类型分别为<special null type>(不同于BooleanE1!)boolean,因此没有特定的键入子句适用(请去读'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)==`布尔
    • lub(T1,T2)== Boolean

    因此,条件表达式的类型为,Boolean并且第三个操作数必须强制为Boolean。编译器为第三个操作数(false)插入自动装箱代码。第二个操作数不需要像E1,因此null返回时没有自动拆箱的NPE 。


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

Java条件运算符?:结果类型


4
有道理...我认为。该§15.12.2.7是一种痛苦。
BalusC,2010年

这很容易...但是只是事后看来。:-)
Bert F

@BertF什么功能lublub(T1,T2)立场?
极客2014年

1
@Geek-lub()-最小上限-基本上是它们共同的最接近的超类;由于null(类型为“特殊null类型”)可以隐式转换(扩展)为任何类型,因此出于lub()的目的,您可以将特殊null类型视为任何类型(类)的“超类”。
Bert F

25

该行:

    Boolean b = true ? returnsNull() : false;

在内部转换为:

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

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

这是使用自动装箱时的主要陷阱之一。这种行为确实记录在 5.1.8 JLS中

编辑:我相信取消装箱是由于第三个运算符是布尔类型,例如(添加了隐式强制转换):

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

2
当最终值是布尔对象时,为什么要这样尝试取消装箱?
艾里克·罗伯逊

16

根据Java语言规范的第15.25节

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

所以,第一个例子试图调用Boolean.booleanValue(),以转化Booleanboolean按第一条规则。

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

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

这可以回答第一种情况,但不能回答第二种情况。
BalusC,2010年

当其中一个值是时,可能会有例外null
艾里克·罗伯逊

@Erick:JLS确认了吗?
BalusC,2010年

1
@Erick:我认为这不是适用的,因为boolean它不是引用类型。
axtavt 2010年

1
并且我想补充一下……这就是为什么您应该使三元数的两边都属于同一类型,并在必要时进行显式调用。即使您记住了规格并知道会发生什么,下一个要来阅读您的代码的程序员也可能不会。以我的拙见,如果编译器只是在这种情况下产生错误消息,而不是做普通人难以预测的事情,那会更好。好吧,也许在某些情况下,这种行为确实有用,但是我还没有打过。
杰伊

0

我们可以从字节码中看到这个问题。在main的字节码的第3行3: invokevirtual #3 // Method java/lang/Boolean.booleanValue:()Z,值为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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.