为什么这个赋值会导致NPE(空指针异常)?

26
public class Npe {
    static class Thing {
        long value;
    }

    public static Map<Thing, Long> map;

    public static void main(String[] args) {
        Thing thing = new Thing();
        method(null); // returns -1
        method(thing); // returns 0
        map = new HashMap<Thing, Long>();
        method(null); // returns -1
        method(thing); // NullPointerException thrown inside this method call
    }

    public static long method(Thing thing) {
        if (thing == null) {
            return -1;
        }
        Long v = (map == null) ? thing.value : map.get(thing); // NPE here
        if (v == null) {
            v = thing.value;
        }
        return v;
    }
}

第四次调用method()时,在method()内部指定行抛出NullPointerException。如果我将该行重构为

Long v = (map == null) ? thing.value : map.get(thing);
to
Long v;
if (map == null) {
    v = thing.value;
} else {
    v = map.get(thing);
}

我没有收到NullPointerException异常,方法的行为也符合预期。问题是:为什么呢?

在我看来,编译器期望?操作符的结果是long,因此它会自动取消装箱(从Long降级为long)对map.get(thing)的调用结果(这可能返回null,从而抛出NullPointerException)。我认为它应该期望?操作符的结果是Long,并将thing.value自动装箱(将long提升为Long)。

更好的做法是,如果我重构这个语句:

Long v = (map == null) ? thing.value : map.get(thing);

long显式转换为Long

Long v = (map == null) ? (Long)thing.value : map.get(thing);

我的IDE(IntelliJ)提示这个强制类型转换是多余的,但编译后代码按预期工作且不会抛出NullPointerException异常! :-D


2
可能是布尔值、条件运算符和自动装箱的重复问题。 - Bhesh Gurung
10
@DwB,你的评论不相关,因为我不是初级程序员。而你关于“不使用三目运算符”的建议也有些愚蠢。仅仅因为某些东西可能很复杂或令人困惑,并不意味着你不应该使用它。这只是意味着你需要小心并知道你在做什么。如果开发人员从未使用过任何复杂或潜在令人困惑的东西,他们就会成为画家、清洁工或园丁,而不是程序员;-) - David Wasser
1
如果你不理解三元运算符,那么不管信不信,你在Java方面仍然是初级水平。此外,某些东西很复杂、令人困惑且没有价值,这是不使用它的一个很好的理由。如果开发人员从不使用任何复杂和潜在令人困惑的东西,他们将会产生更少不必要的复杂和混乱的代码。 - DwB
2个回答

29

考虑您的条件表达式:

(map == null) ? thing.value : map.get(thing)
那个表达式的结果将是 long,因为 thing.value 的类型是 long。请参见JLS §15.25 - 条件运算符。JLS 8 中的表格是一个很好的补充。它澄清了不同输入类型的所有可能输出类型。对于条件表达式的类型引起的混淆很多。 现在当您调用此方法时:
method(thing);

map不为null,因此表达式中的条件map == null求值为false,随后它会计算map.get(thing)的结果。

由于map中还没有条目,所以map.get(thing)将返回null。但是由于结果的类型为long,因此对null进行了拆箱操作,这导致了NPE异常。


现在,当您将thing.value显式转换为Long时,表达式的类型变成了Long。因此,在map.get(thing)的结果上不执行自动拆箱操作,null被分配给Long v


4
Java 的常见陷阱 :) - Jean Logeart
你为什么说表达式的结果会是 long?我认为表达式的结果是 Long,因为第三个参数是 Long - David Wasser
1
@DavidWasser 条件表达式的结果取决于第二个和第三个操作数的类型,而不仅仅是将成为答案的操作数。请参阅JLS链接中的表格。如果任何一个操作数是原始类型,则结果将是原始类型。 - Rohit Jain
2
然而,我仍然坚持认为这是Java的一个bug。没有理由将引用参数“降级”以匹配另一个(原始)参数,而不是将原始参数“提升”以匹配另一个(引用)参数。在我看来,这很愚蠢。这可能会导致NPE,在不必要的情况下没有任何好处,我也看不出它比另一种选择更好。 - David Wasser
4
首先,JLS中明确列出的行为不应被视为错误。任何运算符都必须以某种方式实现。这完全是一个设计决策,并且最好由Java设计师决定如何实现特定运算符。 - Rohit Jain
显示剩余3条评论

0
这是我对发生情况的理解:
当你使用 Long v = (map == null) ? thing.value : map.get(thing); // NPE here 时, map.get(thing) 返回一个 nullLong,然后试图将其值拆箱为 long(因为表达式类型为 long),这会导致 NPE。
但是,当你使用长形式时,你小心地避免了对空 Long 进行拆箱操作。

好的,我正在使用一個版本的IE,它不相信能夠及時顯示SO更新,至少不會在22分鐘內 :) - Bhaskar

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