Java中的Tricky三元运算符 – 自动装箱
我们来看下面代码片段中简单的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代码中,即使函数的返回types是int
, temp()
方法也不会发出编译器错误,我们试图返回值null
(通过语句return true ? null : 0;
)。 编译时,这显然会导致运行时exceptionNullPointerException
。
然而,如果我们使用if
语句(如同same()
方法)表示三元运算符,那么看起来同样的事情是错误的,这会产生编译时错误! 为什么?
编译器将null
解释为对Integer
的空引用,对条件运算符应用自动装箱/拆箱规则(如Java语言规范15.25中所述 ),并愉快地移动。 这会在运行时产生一个NullPointerException
,你可以通过尝试来确认。
我认为,Java编译器解释为true ? null : 0
true ? null : 0
作为Integer
expression式,可以隐式转换为int
,可能会给出NullPointerException
。
对于第二种情况,expression式null
是特殊的空types ,所以代码return null
使得types不匹配。
实际上,它们都是在Java语言规范中解释的。
条件expression式的types如下确定:
- 如果第二个和第三个操作数具有相同的types(可能是空types),那么这就是条件expression式的types。
因此你的(true ? null : 0)
的“null”得到一个inttypes,然后被自动装箱到Integer。
尝试这样的事情来validation这个(true ? null : null)
,你会得到编译器错误。
在if
语句的情况下, null
引用不被视为Integer
引用,因为它不参与强制它被解释的expression式 。 因此,错误可以在编译时很容易被捕获,因为它更清楚地是一个types错误。
至于条件运算符,Java语言规范§15.25“有条件运算符? :
? :
“在如何应用types转换的规则中很好地回答了这个问题:
- 如果第二个和第三个操作数具有相同的types(可能是空types),那么这就是条件expression式的types。
不适用,因为
null
不是int
。
- 如果第二个和第三个操作数中的一个是布尔types,而另一个的types是布尔types,则条件expression式的types是布尔值。
不适用,因为既不
null
也不int
是boolean
或Boolean
。
- 如果第二个和第三个操作数中的一个是空types,而另一个的types是引用types,那么条件expression式的types就是该引用types。
不适用,因为
null
是空types,但是int
不是引用types。
- 否则,如果第二个和第三个操作数的types可以转换(§5.1.8)为数字types,那么有几种情况:[…]
应用:
null
被视为可转换为数字types,并在§5.1.8“取消装箱转换”中定义,以引发NullPointerException
。
首先要记住的是,Java三元运算符有一个“types”,这就是编译器决定和考虑的内容,而不pipe第二个或第三个参数的实际types是什么。 根据几个因素,三元运算符types以Java语言规范15.26中所示的不同方式确定
在上面的问题中,我们应该考虑最后一种情况:
否则,第二个和第三个操作数分别是S1和S2的types。 假设T1是将装箱转换为S1所得到的types,并设T2是将装箱转换为S2所得到的types。 条件expression式的types是将采集转换(§5.1.10)应用于lub(T1,T2) (§15.12.2.7)的结果。
这是迄今为止最复杂的情况,一旦你看看应用捕获转换(§5.1.10) ,最重要的是在lub(T1,T2) 。
用简单的英语,经过极端的简化,我们可以把这个过程描述为计算第二个和第三个参数的“最小公共超类”(是的,考虑LCM)。 这将给我们三元操作符“types”。 同样,我刚刚说的是一个极端的简化(考虑实现多个通用接口的类)。
例如,如果您尝试以下操作:
long millis = System.currentTimeMillis(); return(true ? new java.sql.Timestamp(millis) : new java.sql.Time(millis));
您会注意到,条件expression式的结果types是java.util.Date
因为它是Timestamp
/ Time
对的“最less公共超类”。
由于null
可以自动复制到任何东西,所以“最小公共超类”是Integer
类,这将是上面的条件expression式(三元运算符)的返回types。 返回值将是一个Integer
types的空指针,三元运算符将会返回这个值。
在运行时,当Java虚拟机解开Integer
时,抛出一个NullPointerException
exception。 发生这种情况是因为JVM尝试调用函数null.intValue()
,其中null
是自动装箱的结果。
在我看来(因为我的意见不在Java语言规范中,很多人会发现它是错误的)编译器在评估expression式的时候做得不好。 鉴于你写的是true ? param1 : param2
true ? param1 : param2
编译器应该立即确定将返回第一个参数 – null
– 它应该会产生一个编译器错误。 这有些类似于你编写while(true){} etc...
,编译器会抱怨循环下面的代码,并用Unreachable Statements
标记它。
你的第二种情况是相当简单的,这个答案已经太长了;;)
更正:
经过另一个分析,我认为我错了,说一个null
值可以装箱/自动复制到任何东西。 说到Integer类,显式装箱包括调用new Integer(...)
构造函数或者Integer.valueOf(int i);
(我发现这个版本的地方)。 前者会抛出一个NumberFormatException
(这不会发生),而第二个将没有意义,因为一个int
不能为null
…
实际上,在第一种情况下,可以评估expression式,因为编译器知道它必须被评估为Integer
,但是在第二种情况下返回值( null
)的types不能被确定,所以它不能被编译。 如果将其转换为Integer
,则代码将被编译。
private int temp() { if (true) { Integer x = null; return x;// since that is fine because of auto-boxing 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 }
这个怎么样:
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); } }
输出是真实的。
Eclipse颜色将条件expression式中的1编码为自动复制。
我的猜测是编译器看到expression式的返回types为Object。