使用三元运算符允许返回 null 作为 int,但不允许 if 语句

Returning null as an int permitted with ternary operator but not if statement

提问人:Lion 提问时间:11/12/2011 最后编辑:RaedwaldLion 更新时间:6/12/2019 访问量:17551

问:

让我们看一下以下代码片段中的简单 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()intnullreturn true ? null : 0;NullPointerException

但是,如果我们用语句表示三元运算符(如方法中所示),这似乎是错误的,这确实会发出编译时错误!为什么?ifsame()

java nullpointerException 条件运算符 自动装箱

评论

6赞 Izkata 11/12/2011
此外,两者都编译得很好,第二个是自动装箱的显式形式。int foo = (true ? null : 0)new Integer(null)
2赞 Marsellus Wallace 11/12/2011
@Izkata这里的问题是让我理解为什么编译器试图自动装箱......对我来说,这看起来就像“猜测”或“让事情顺利进行”......nullInteger
1赞 Izkata 11/12/2011
...呵呵,我以为我在那里有一个答案,因为 Integer 构造函数(我发现的文档说用于自动装箱)被允许将 String 作为参数(可以为 null)。但是,他们也说构造函数的行为与 parseInt() 方法相同,parseInt() 方法在传递 null 时会抛出 NumberFormatException......
3赞 Ted Hopp 11/13/2011
@Izkata - Integer 的 String 参数 c'tor 不是自动装箱操作。字符串不能自动装箱为 Integer。(该函数不会编译。Integer foo() { return "1"; }
5赞 oksayt 11/17/2011
很酷,学到了一些关于三元运算符的新知识!

答:

40赞 Vlad 11/12/2011 #1

我认为,Java 编译器解释为一个表达式,可以隐式转换为 ,可能给出 .true ? null : 0IntegerintNullPointerException

对于第二种情况,表达式属于特殊的 null 类型 see,因此代码会使类型不匹配。nullreturn null

评论

2赞 Michael McGowan 11/12/2011
我认为这与自动装箱有关?据推测,第一个返回值不会在 Java 5 之前编译,对吧?
0赞 Jonathon Faust 11/12/2011
@Michael,如果将 Eclipse 的合规性级别设置为 pre-5,则情况似乎如此。
0赞 Vlad 11/12/2011
@Michael:这绝对看起来像是自动装箱(我对 Java 很陌生,无法做出更明确的声明——对不起)。
1赞 Marsellus Wallace 11/12/2011
@Vlad编译器最终会如何解释为?先自动装箱??true ? null : 0Integer0
1赞 Vlad 11/12/2011
@Gevorg:看这里否则,第二个和第三个操作数分别属于 S1 和 S2 类型。设 T1 是将装箱转换应用于 S1 所生成的类型,并设 T2 是将装箱转换应用于 S2 所生成的类型。和以下文本。
120赞 Ted Hopp 11/12/2011 #2

编译器将 解释为 对 的空引用,应用条件运算符的自动装箱/取消装箱规则(如 Java 语言规范 15.25 中所述),然后愉快地继续前进。这将在运行时生成一个,您可以通过尝试来确认。nullIntegerNullPointerException

评论

0赞 Marsellus Wallace 11/12/2011
鉴于您发布的 Java 语言规范的链接,您认为在上述问题的情况下执行哪一点?最后一个(因为我还在努力理解和)??另外,真的可以将装箱应用于空值吗?这难道不像“猜测”吗??capture conversionlub(T1,T2)
0赞 Voo 11/13/2011
'@Gevorg 空指针是指向每个可能对象的有效指针,因此那里不会发生任何坏事。编译器只是假设 null 是一个 Integer,然后它可以自动装箱为 int。
1赞 Ted Hopp 11/13/2011
@Gevorg - 参见 nowaq 的评论和我对他的帖子的回应。我认为他选择了正确的条款。 是 T1 和 T2 的类型层次结构中最常见的最具体的引用类型。(它们都至少共享 Object,因此始终存在最具体的引用类型。lub(T1,T2)
8赞 Ted Hopp 11/15/2011
@Gevorg - 没有被装箱到 Integer 中,它被解释为对 Integer 的引用(null 引用,但这不是问题)。没有 Integer 对象是从 null 构造的,因此没有理由出现 NumberFormatException。null
1赞 Ted Hopp 11/19/2011
@Gevorg - 如果您查看装箱转换的规则,并将它们应用于(不是原始数值类型),则适用的子句是“如果 p 是任何其他类型的值,则装箱转换等同于标识转换”。因此,在不调用任何构造函数的情况下将 to yield 的 boxing 转换。nullnullIntegernullInteger
32赞 nowaq 11/12/2011 #3

实际上,这一切都在 Java 语言规范中进行了解释。

条件表达式的类型确定如下:

  • 如果第二个和第三个操作数具有相同的类型(可能是 null 类型),则这就是条件表达式的类型。

因此,your 中的 “null” 得到一个 int 类型,然后被自动装箱为 Integer。(true ? null : 0)

尝试这样的东西来验证这一点,你会得到编译器错误。(true ? null : null)

评论

3赞 Ted Hopp 11/12/2011
但是规则的这一条款并不适用:第二个和第三个操作数的类型不同
1赞 nowaq 11/12/2011
那么答案似乎在下面的语句中: > 否则,第二个和第三个操作数分别属于 S1 和 S2 类型。设 T1 是将装箱转换应用于 S1 所生成的类型,并设 T2 是将装箱转换应用于 S2 所生成的类型。条件表达式的类型是将捕获转换 (§5.1.10) 应用于 lub(T1, T2) (§15.12.2.7) 的结果。
0赞 Ted Hopp 11/13/2011
我认为这是适用的条款。然后,它尝试应用自动拆箱,以便从函数返回一个值,这会导致 NPE。int
0赞 Marsellus Wallace 11/15/2011
@nowaq我也这么认为。但是,如果尝试使用“让 T1 成为将装箱转换为 S1 所产生的类型...”显式装箱。你会得到一个,但事实并非如此......nullIntegernew Integer(null);NumberFormatException
0赞 Voo 11/16/2011
@Gevorg我认为,由于在进行拳击时发生了异常,我们在这里没有得到任何结果。编译器只是有义务生成遵循它所做的定义的代码 - 我们只是在完成之前得到异常。
25赞 Jon Purdy 11/12/2011 #4

在语句的情况下,引用不被视为引用,因为它没有参与强制将其解释为引用的表达。因此,该错误很容易在编译时捕获,因为它更明显地是类型错误。ifnullInteger

至于条件运算符,Java 语言规范 §15.25 “条件运算符 ” 在如何应用类型转换的规则中很好地回答了这个问题:? :

  • 如果第二个和第三个操作数具有相同的类型(可能是 null type),则这是条件表达式的类型。

    不适用,因为 null 不是 int

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

    不适用,因为 nullint 都不是布尔值或布尔值

  • 如果第二个和第三个操作数之一为 null 类型,并且 other 是引用类型,那么条件表达式的类型是 引用类型。

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

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

    适用:null 被视为可转换为数值类型,并在 §5.1.8“取消装箱转换”中定义以引发 NullPointerException

评论

0赞 Marsellus Wallace 11/12/2011
如果自动装箱,则编译器将执行 Java 语言规范中描述的“三元运算符规则”的最后一种情况。如果这是真的,那么我很难相信它会跳到相同规则的第 3 种情况,该规则具有 null 和引用类型,使三元运算符的返回值成为引用类型(整数)......0Integer
0赞 Ted Hopp 11/13/2011
@Gevorg - 为什么很难相信三元运算符返回的 ?这正是正在发生的事情;NPE 是通过尝试拆箱表达式值来生成的,以便从函数返回 。将函数更改为返回 an,它将毫无问题地返回。IntegerintIntegernull
2赞 Jon Purdy 11/13/2011
@TedHopp:Gevorg 正在回应我回答的早期修订版,这是不正确的。您应该忽略这种差异。
0赞 Marsellus Wallace 11/19/2011
@JonPurdy “如果一个类型是数值类型,或者它是可以通过拆箱转换转换为数值类型的引用类型,则称它为可转换为数值类型”,我认为这不属于这一类。此外,我们将进入“否则,将应用二进制数字提升 (§5.6.2) ......请注意,二进制数字升级执行拆箱转换 (§5.1.8) ...”步骤以确定返回类型。但是拆箱转换会生成 NPE,这只会在运行时发生,而不是在尝试确定三元运算符类型时发生。我还是很困惑..null
0赞 Jon Purdy 11/19/2011
@Gevorg:拆箱在运行时进行。被视为具有类型,但实际上等价于 ,仅此而已。nullintthrow new NullPointerException()
11赞 Marsellus Wallace 11/12/2011 #5

首先要记住的是,Java 三元运算符有一个“类型”,无论第二个或第三个参数的实际/实际类型是什么,编译器都会确定和考虑这一点。根据几个因素,三元运算符类型以不同的方式确定,如 Java 语言规范 15.26 中所示

在上面的问题中,我们应该考虑最后一种情况:

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

这是迄今为止最复杂的情况,一旦您查看了应用捕获转换 (§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.DateTimestampTime

由于可以自动装箱到任何内容,因此“最小公共超类”是类,这将是上面条件表达式(三元运算符)的返回类型。然后,返回值将是类型的 null 指针,这是三元运算符返回的内容。nullIntegerInteger

在运行时,当 Java 虚拟机拆箱时,会抛出 a。发生这种情况是因为JVM尝试调用函数,其中是自动装箱的结果。IntegerNullPointerExceptionnull.intValue()null

在我看来(由于我的观点不在 Java 语言规范中,所以很多人无论如何都会发现它是错误的),编译器在评估您问题中的表达式方面做得很差。假设您编写了编译器,编译器应立即确定将返回第一个参数 -- 并将生成编译器错误。这有点类似于您编写时,编译器抱怨循环下面的代码并用 .true ? param1 : param2nullwhile(true){} etc...Unreachable Statements

你的第二个案例很简单,这个答案已经太长了...... ;)

校正:

经过另一次分析,我认为我说一个值可以装箱/自动装箱到任何东西是错误的。谈到 Integer 类,显式装箱包括调用构造函数或(我在某处找到了这个版本)。前者会抛出一个(这不会发生),而第二个则没有意义,因为不能......nullnew Integer(...)Integer.valueOf(int i);NumberFormatExceptionintnull

评论

1赞 Ted Hopp 11/16/2011
OP 的原始代码未装箱。它的工作方式是:编译器假定 是对 Integer 的引用。使用三元表达式类型的规则,它确定整个表达式是整数表达式。然后,它生成代码以自动装箱(如果条件的计算结果为 )。在执行期间,条件的计算结果为 ,因此表达式的计算结果为 。当尝试从函数返回 时,是 被拆箱的。然后抛出 NPE。(编译器可能会优化其中的大部分内容。nullnull1falsetruenullintnull
4赞 GeT 11/16/2011 #6

实际上,在第一种情况下,表达式可以被计算,因为编译器知道,它必须被计算为 ,但是在第二种情况下,无法确定返回值 () 的类型,因此无法编译它。如果将其转换为 ,则代码将编译。IntegernullInteger

2赞 Youans 12/11/2013 #7
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赞 Jon 9/9/2016 #8

这个怎么样:

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。