使用三元运算符返回null作为int类型是允许的,但在if语句中不被允许。

190

让我们来看一下下面代码段中的简单Java代码:

public class Main {

    private int temp() {
        return true ? null : 0;
        // No compiler error - the compiler allows a return value of null
        // in a method signature that returns an int.
    }

    private int same() {
        if (true) {
            return null;
            // The same is not possible with if,
            // and causes a compile-time error - incompatible types.
        } else {
            return 0;
        }
    }

    public static void main(String[] args) {
        Main m = new Main();
        System.out.println(m.temp());
        System.out.println(m.same());
    }
}
在这个最简单的Java代码中,temp()方法没有编译器错误,即使函数的返回类型是int,我们试图通过语句 return true?null:0; 返回值为null。当编译时,显然会导致运行时异常NullPointerException
然而,如果我们使用一个if语句代替三元运算符表示(如same() 方法中),相同的事情看起来就错了,这样做会发出编译时错误!为什么呢?

6
int foo = (true ? null : 0)new Integer(null) 都可以编译通过,后者是自动装箱的显式形式。 - Izkata
2
@Izkata 这里的问题是我不理解编译器为什么要将 null 自动装箱为 Integer... 对我来说这看起来就像是“猜测”或“让事情正常工作”... - Marsellus Wallace
1
... 嗯,我本以为我有个答案,因为我找到的文档说 Integer 构造函数(用于自动装箱)可以接受 String 作为参数(其可以为 null)。然而,他们也说构造函数的行为与 parseInt() 方法完全相同,后者会在传入 null 时抛出 NumberFormatException 异常... - Izkata
3
@Izkata - Integer的String参数构造函数不是自动装箱操作。一个String不能自动装箱为Integer。(函数Integer foo() { return "1"; }无法编译通过。) - Ted Hopp
5
酷,我对三元运算符学到了新东西! - oksayt
显示剩余2条评论
8个回答

120

编译器将null解释为对一个Integer的空引用,并按照条件运算符(如Java语言规范 15.25中所述)的自动装箱/拆箱规则进行处理,然后毫不犹豫地继续执行。这将在运行时生成一个NullPointerException,你可以尝试验证一下。


1
@Gevorg - 请看nowaq的评论以及我的回复。我认为他选择了正确的子句。lub(T1,T2)是T1和T2类型层次结构中最常见的最具体的引用类型。(它们都至少共享Object,因此总有一个最具体的引用类型。) - Ted Hopp
8
@Gevorg - null不会被装箱成一个整数,而是被解释为对整数的引用(空引用,但这并不是问题)。没有从null构造出整数对象,因此不存在NumberFormatException的原因。 - Ted Hopp
1
@Gevorg - 如果您查看装箱转换规则,并将其应用于null(它不是原始数值类型),适用的条款是“如果_p_是任何其他类型的值,则装箱转换等同于标识转换”。因此,将null装箱转换为Integer会产生null,而不会调用任何Integer构造函数。 - Ted Hopp
在我看来,实际上发生的情况是编译器只进行了装箱转换,得到了一个空 Int 和一个零 Int。但是,拆箱 null 会抛出 null 指针异常,所以当它尝试在运行时拆箱 null Int 时,就会出现该错误。这可能就是你所说的。说“自动装箱/拆箱”让人觉得两者都发生在编译时,但我认为你只是指一般规则,包括这两个方面,但是上面的代码只进行了装箱。 - user4843530
@Anon316 - 其实,装箱和拆箱转换都发生在运行时。对于三元运算符,编译器将直接将null推送到堆栈中,或者推送0,然后调用静态方法 Integer.valueOf()(它将堆栈上的顶部元素替换为对Integer对象的引用)。 然后,它生成字节码通过实例方法调用Integer.intValue()以使用假定的栈上的Integer对象引用获取返回值。 当运行时发现对象引用为null时,将触发NPE异常。 - Ted Hopp
显示剩余6条评论

40

我认为 Java 编译器将 true ? null : 0 解释为一个 Integer 表达式,该表达式可以被隐式转换为 int,可能会导致 NullPointerException

对于第二种情况,表达式 null 属于特殊的 null 类型参见,所以代码 return null 会产生类型不匹配的错误。


2
我猜这与自动装箱有关?大概在Java 5之前,第一个返回值是不会编译通过的,对吧? - Michael McGowan
@Michael:这绝对是自动装箱(我对Java还很新,不能做出更明确的陈述——抱歉)。 - Vlad
1
@Vlad,编译器如何将true?null:0解释为 Integer呢?会通过自动装箱先将0转换为Integer类型吗? - Marsellus Wallace
1
@Gevorg:看这里:http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#15.25:_否则,第二个和第三个操作数分别是类型S1和S2。让T1成为将S1应用装箱转换所得到的类型,让T2成为将S2应用装箱转换所得到的类型。_以及以下文本。 - Vlad
@Vlad 对,那应该是语言规范的最后一个情况,并且也是我最好的猜测。但我很难相信编译器会跳到第三点,因为我认为它应该按照那些“if,otherwise”的建议按顺序检查不同的可能性...lub(T1,T2)对我来说相当难以理解。 - Marsellus Wallace
显示剩余6条评论

32
实际上,这个问题在Java语言规范中有详细解释。
条件表达式的类型如下确定:
如果第二个和第三个操作数具有相同的类型(可以是空类型),则该条件表达式的类型为该类型。
因此你的 (true ? null : 0) 中的 "null" 获得了 int 类型,然后自动装箱为 Integer。
尝试像这样验证 (true ? null : null),你将会得到编译器错误。

3
规则的那个条款不适用:第二和第三操作数 不具有相同的类型。 - Ted Hopp
1
那么答案似乎在以下语句中: 否则,第二个和第三个操作数分别是类型S1和S2。让T1成为将S1应用装箱转换所得到的类型,让T2成为将S2应用装箱转换所得到的类型。条件表达式的类型是将lub(T1,T2)(§15.12.2.7)应用于捕获转换(§5.1.10)的结果。 - nowaq
我认为这是适用的条款。 然后它尝试应用自动拆箱以从函数返回int值,这会导致NPE。 - Ted Hopp
@nowaq 我也曾这样想过。但是,如果你尝试使用 new Integer(null); 显式地将 null 转换为 Integer,根据 "将 S1 应用装箱转换后得到的类型为 T1..." 的规则,你会得到一个 NumberFormatException,但事实并非如此... - Marsellus Wallace
@Gevorg 我认为由于装箱操作引发了异常,所以我们在这里得不到任何结果。编译器只是被迫生成遵循定义的代码,它确实这样做了 - 我们只是在完成之前得到了异常。 - Voo
@Voo - 在装箱时不会生成任何异常。异常发生在_un_boxing null以便从方法返回int值时。如果方法签名更改为返回Integer,则根本不会有异常--它只会返回“null”。 - Ted Hopp

25
if 语句的情况下,null 引用不会被视为 Integer 引用,因为它没有参与强制将其解释为这样的表达式。因此,该错误可以在编译时轻松捕获,因为它更清楚地是一种类型错误。
至于条件运算符,Java 语言规范 §15.25 “Conditional Operator ? :” 在应用类型转换的规则中很好地回答了这个问题:
  • 如果第二个和第三个操作数具有相同的类型(可能是 null 类型),那么它就是条件表达式的类型。

    不适用,因为 null 不是 int

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

    不适用,因为 nullint 都不是 booleanBoolean

  • 如果第二个和第三个操作数之一是 null 类型,而另一个操作数的类型是引用类型,则条件表达式的类型是该引用类型。

    不适用,因为 null 是 null 类型,但是 int 不是引用类型。

  • 否则,如果第二个和第三个操作数具有可以转换为数字类型(§5.1.8)的类型,则有以下几种情况:[...]

    适用范围:null被视为可转换为数值类型,并在§5.1.8“解封装转换”中定义为抛出NullPointerException

如果0被自动装箱为Integer,那么编译器将执行Java语言规范中描述的“三元运算符规则”的最后一个case。如果是这样的话,那么我很难相信它会跳转到同一规则的第3个case,其中包含一个null和一个引用类型,使得三元运算符的返回值成为引用类型(Integer)... - Marsellus Wallace
@Gevorg - 为什么很难相信三元运算符返回一个Integer?这正是正在发生的事情;NPE是通过尝试取消框定表达式值以从函数返回'int'而生成的。将函数更改为返回Integer,它将毫无问题地返回null - Ted Hopp
2
@TedHopp:Gevorg是在回应我之前错误的答案修订版。您应该忽略这个差异。 - Jon Purdy
@JonPurdy说:“如果一个类型是数字类型,或者它是一个引用类型,可以通过拆箱转换转换为数字类型,则称该类型可转换为数字类型”,我认为null不属于这个范畴。此外,我们将进入“否则,应用二进制数提升(§5.6.2)...请注意,二进制数提升执行拆箱转换(§5.1.8)...”步骤来确定返回类型。但是,拆箱转换会生成NPE,这只会在运行时发生,而不是在尝试确定三元运算符类型时发生。我仍然感到困惑... - Marsellus Wallace
@Gevorg:拆箱操作发生在运行时。null 被视为具有 int 类型,但实际上等同于 throw new NullPointerException(),仅此而已。 - Jon Purdy

11
首先需要记住的是,Java三元运算符具有“类型”,而编译器将确定此类型并考虑它,无论第二或第三个参数的实际/真实类型是什么。根据几个因素,三元运算符类型的确定方式不同,如Java语言规范15.26所示。
在上面的问题中,我们应该考虑最后一种情况:
否则,第二个和第三个操作数的类型分别为S1和S2。让T1是将装箱转换应用于S1的结果类型,让T2是将装箱转换应用于S2的结果类型。条件表达式的类型是对lub(T1,T2)(§15.12.2.7)应用捕获转换(§5.1.10)的结果。
这是目前为止最复杂的情况之一,一旦你查看了应用捕获转换(§5.1.10),尤其是lub(T1,T2)
用简单的语言描述这个过程并进行极端简化后,我们可以将其描述为计算第二个和第三个参数的“最小公共超类”(是的,请考虑LCM)。这将给我们三元运算符的“类型”。再次强调,我刚才说的是一个极端简化(请考虑实现多个公共接口的类)。
例如,如果您尝试以下操作:
long millis = System.currentTimeMillis();
return(true ? new java.sql.Timestamp(millis) : new java.sql.Time(millis));
你会发现条件表达式的结果类型为java.util.Date,因为它是Timestamp/Time对的“最小公共超类”。由于null可以自动装箱成任何类型,所以“最小公共超类”是Integer类,并且这将是上述条件表达式(三元运算符)的返回类型。然后返回值将是一个Integer类型的空指针,这就是三元运算符返回的内容。在运行时,当Java虚拟机取消装箱Integer时,会抛出NullPointerException。这是因为JVM尝试调用null.intValue()函数,其中null是自动装箱的结果。在我的看法中(因为我的观点不在Java语言规范中,许多人都会发现它是错误的),编译器在评估你的问题时做得很差。鉴于你写的是true?param1:param2表达式,编译器应该立即确定将返回第一个参数-null,并生成编译器错误。这与您编写while(true){}等...代码时相似,编译器会抱怨循环下面的代码并使用Unreachable Statements标记它。第二个案例非常简单,这个答案已经太长了... ;)。 更正: 经过另一次分析,我认为我说null值可以被装箱/自动装箱成任何类型是错误的。谈到Integer类,显式装箱是调用new Integer(...)构造函数或者可能是Integer.valueOf(int i)(我在某处找到了这个版本)。前者会抛出NumberFormatException(但这并没有发生),而后者则没有意义,因为int不能为null...

1
OP原始代码中的null未被装箱。其工作方式是:编译器假定null是对整数的引用。使用三元表达式类型的规则,它决定整个表达式是一个整数表达式。然后,它生成代码来自动装箱1(以防条件求值为“false”)。在执行期间,条件求值为“true”,因此表达式求值为null。当尝试从函数返回int时,将取消装箱null。这将引发NPE。(编译器可能会优化掉大部分内容。) - Ted Hopp

4

实际上,在第一种情况下,表达式可以被计算,因为编译器知道它必须被计算为一个 Integer,然而在第二种情况下,返回值的类型 (null) 无法确定,所以它无法被编译。如果将其强制转换为 Integer,那么代码就可以编译。


2
private int temp() {

    if (true) {
        Integer x = null;
        return x;// since that is fine because of unboxing then the returned value could be null
        //in other words I can say x could be null or new Integer(intValue) or a intValue
    }

    return (true ? null : 0);  //this will be prefectly legal null would be refrence to Integer. The concept is one the returned
    //value can be Integer 
    // then null is accepted to be a variable (-refrence variable-) of Integer
}

0
这样怎么样:
public class ConditionalExpressionType {

    public static void main(String[] args) {

        String s = "";
        s += (true ? 1 : "") instanceof Integer;
        System.out.println(s);

        String t = "";
        t += (!true ? 1 : "") instanceof String;
        System.out.println(t);

    }

}

输出结果为true,true。

Eclipse将条件表达式中的1着色为自动装箱。

我的猜测是编译器将表达式的返回类型视为Object。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接