Java's Ternary Is Tricky With Autoboxing/Unboxing
Let's take a closer look at why Java handles boxing with its ternary operator the way it does.
Join the DZone community and get the full member experience.
Join For FreeThe comments section of the DZone-syndicated version of my post "JDK 8 Versus JDK 10: Ternary/Unboxing Difference" had an interesting discussion regarding the "why" behind the "fix" for how Java handles autoboxing/unboxing in conjunction with the use of the ternary operator (AKA the "conditional operator"). This post expands on that discussion with a few more details.
One of the points made in the discussion is that the logic for how primitives and reference types are handled in a ternary operator; in particular, when autoboxing or unboxing is required, it can be less than intuitive. For compelling evidence of this, one only needs to look at the number of bugs written for perceived problems with Java's conditional operator's behavior where autoboxing and unboxing are involved:
JDK-6211553: Unboxing in conditional operators might cause a null pointer exception
JDK-6303028 : Conditional operator + autoboxing throws NullPointerException
The type of the conditional operator
(s == null) ? (Long) null : Long.parseLong(s)
is the primitive type long, not java.lang.Long.
This follows from the JLS, 3rd ed, page 511:
"Otherwise, binary numeric promotion (5.6.2) is applied to the operand
types, and the type of the conditional expression is the promoted type of the
second and third operands. Note that binary numeric promotion performs
unboxing conversion (5.1.8) and value set conversion (5.1.13)."
In particular, this means that (Long)null is subjected to unboxing conversion.
This is the source of the null pointer exception.
JDK-8150614: conditional operators, null-argument only for return purpose, and the NullPointerException
in the "Comments" section explains, "The code is running afoul of the complicated rules for typing of the ?: operator" and references the pertinent section of the Java Language Specification for the current version at the time of that writing.
I like the explanation on this one as well: "The code in the bug has one branch of the ?:
typed as an Integer
(with the 'replace' variable") and the other branch typed as an int
from Integer.parseInt
. In that case, an unboxing Integer
-> int
conversion will occur before a boxing to the final result, leading to the NPE. To avoid this, case the result of parseInt
to Integer
."
The "Comments" section concludes: "Closing as not a bug."
JDK-6777143: NullPointerException occurred at conditional operator The "EVALUATION" section of this bug report provides an interesting explanation with a historical perspective: it is because of NPEs that JLS 15.25 says 'Note that binary numeric promotion performs unboxing conversion'. The potential for NullPointerException
and OutOfMemoryError
's in 1.5 where they could never have occurred in 1.4 was well known to the JSR 201 expert group. It could have made the unboxing conversion from the null type infer the target type from the context (and have the unboxed value be the default value for that type), but the inference was not common before 1.5 expanded the type system and it's certainly not going to happen now.
JDK-6360739: Tertiary operator throws NPE, due to redundant casting. It's no wonder it's not intuitive to many of us! Section 15.25 ("Conditional Operator ? :") of the Java Language Specification is the defining authority regarding the behavior of the ternary operator with regards to many influences, including autoboxing and unboxing. This is the section referenced in several of the bug reports cited above and in some of the other resources that I referenced in my original post. It's worth noting that this section of the PDF version of the Java SE 10 Language Specification is approximately 9 pages!
In the DZone comments on my original post, Peter Schuetze and Greg Brown reference Table 15.25-D from the Java Language Specification for the most concise explanation of the misbehavior in JDK 8 that was rectified in JDK 10. I agree with them that this table is easier to understand than the accompanying text illustrated by the table. That table shows the type of the overall ternary operation based on the types of the second expression and third expression (where the second expression is the expression between the ?
and :
and the third expression is the expression following the :
as shown next):
first expression ? second expression : third expression
The table's rows represent the type of the second expression and the table's columns represent the type of the third expression. One can find where the types meet in the table to know the overall type of the ternary operation. When one finds the cell of the table that correlates to row of primitive double
and column of reference Double
, the cell indicates that the overall type is primitive double
. This is why the example shown in my original post should throw a NullPointerException
, but was in violation of the specification in JDK 8 when it did not do so.
I sometimes wonder if autoboxing and unboxing are a case of the "cure being worse than the disease." However, I have found autoboxing and unboxing to be less likely to lead to subtle errors, if I'm careful about when and how I use those features. A J articulates it well in his comment on the DZone version of my post: "The practical takeaway I got from this article is: when presented with an incomprehensible error, if you see that you are relying on autoboxing in that area of code (i.e., automatic type conversion), do the type conversion yourself manually. Then you will be sure the conversion is being done right."
Published at DZone with permission of Dustin Marx, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments