Before Java8, in almost all cases† , the type of an expression is built bottom-up, entirely depending on the types of sub-expressions; it does not depend on the context. This is nice and simple, and code is easy to understand; for example, overload resolution depends on the types of arguments, which are resolved independent of the method invocation context.
(† The only exception that I know of is jls#15.12.2.8)
Given a conditional expression in the form of ?int:Integer
, the spec needs to define a fixed type for it with no regard to the context. The int
type was chosen , which is presumably better in most use cases.
Of course, it is also the source of NPE from unboxing.
In Java8, contextual type information may be used in type inference. This is convenient for many cases; but it also introduces confusion, since there may be two directions to resolve the type of an expression. Luckily, some expressions are still stand-alone; their types are context-independent.
w.r.t conditional expressions, we don't want simple ones like false?0:1
to be context-dependent; their types are self-evident. On the other hand, we do want contextual type inference on more complicated conditional expressions, like false?f():g()
where f/g()
requires type inference.
The line was drawn between primitive and reference types. In op1?op2:op3
, if both op2
and op3
are "clearly" primitive types (or boxed versions of), it is treated as stand-alone. Quoting Dan Smith -
We classify conditional expressions here in order to enhance the typing rules of reference conditionals (15.25.3) while preserving existing behavior of boolean and numeric conditionals. If we tried to treat all conditionals uniformly, there would be a variety of unwanted incompatible changes, including changes in overload resolution and boxing/unboxing behavior.
In your case
Integer x = false ? 3 : false ? 4 : null;
since false?4:null
is "clearly"(?) an Integer
, the parent expression is in the form of ?:int:Integer
; this is a primitive case, and its behavior is kept compatible with java7, hence, NPE.
I put quotes on "clearly" because that's my intuitive understanding; I'm not sure about the formal spec. Let's look at this example
static <T> T f1(){ return null; }
--
Integer x = false ? 3 : false ? f1() : null;
It compiles! and there is no NPE at runtime! I don't know how to follow the spec on this case. I can imagine that the compiler probably does the following steps:
1) sub-expression false?f1():null
is not "clearly" a (boxed)primitive type; its type is unknown yet
2) therefore, the parent expression is classified as "reference conditional expression", which appears in an assignment context.
3) the target type Integer
is applied to the operands, and eventually to f1()
, which is then inferred to return Integer
4) however, we cannot go back now to re-classify the conditional expression as ?int:Integer
.
That sounds reasonable. However, what if we explicitly specify the type argument to f1()
?
Integer x = false ? 3 : false ? Test.<Integer>f1() : null;
Theory (A) - this should not alter the semantics of the program, because it's the same type argument that would have been inferred. We should not see NPE at runtime.
Theory (B) - there is no type inference; the type of sub-expression is clearly Integer
, therefore this should be classified as primitive case, and we should see NPE at runtime.
I believe in (B); however, javac(8u60) does (A). I don't understand why.
Pushing this observation to a hilarious level
class MyList1 extends ArrayList<Integer>
{
//inherit public Integer get(int index)
}
class MyList2 extends ArrayList<Integer>
{
@Override public Integer get(int index)
{
return super.get(0);
}
}
MyList1 myList1 = new MyList1();
MyList2 myList2 = new MyList2();
Integer x1 = false ? 3 : false ? myList1.get(0) : null; // no NPE
Integer x2 = false ? 3 : false ? myList2.get(0) : null; // NPE !!!
That doesn't make any sense; something really funky is going on inside javac.
(see also Java autoboxing and ternary operator madness)