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

8

190

Let's look at the simple Java code in the following snippet:

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());
    }
}

In this simplest of Java code, the temp() method issues no compiler error even though the return type of the function is int, and we are trying to return the value null (through the statement return true ? null : 0;). When compiled, this obviously causes the run time exception NullPointerException.

However, it appears that the same thing is wrong if we represent the ternary operator with an if statement (as in the same() method), which does issue a compile-time error! Why?

Lengel answered 11/11, 2011 at 19:30 Comment(7)
Also, int foo = (true ? null : 0) and new Integer(null) both compile fine, the second being the explicit form of autoboxing.Budde
@Budde the problem here is for me to understand why the compiler is trying to autobox null to Integer... That would look just like "guessing" to me or "making things work"...Stenophyllous
...Huhm, I thought I had an answer there, as the Integer constructor (what the docs I found say is used for autoboxing) is allowed to take a String as an argument (which can be null). However, they also say that the constructor acts identically to the method parseInt(), which would throw a NumberFormatException upon getting passed a null...Budde
@Budde - the String argument c'tor for Integer is not an autoboxing oepration. A String can't be autoboxed to an Integer. (The function Integer foo() { return "1"; } won't compile.)Monro
Cool, learned something new about the ternary operator!Gottwald
Also worth noting: return (Integer)null; compiles fineGutty
\o\ Yay to the Runtime NullPointerExceptions /o/ Intriguing question tho, I wonder if will these things be ever changed. like public void foo() { try { foo(); } finally { foo(); } } being a nearly endless recursive function.Browband
M
120

The compiler interprets null as a null reference to an Integer, applies the autoboxing/unboxing rules for the conditional operator (as described in the Java Language Specification, 15.25), and moves happily on. This will generate a NullPointerException at run time, which you can confirm by trying it.

Monro answered 11/11, 2011 at 19:36 Comment(11)
Given the link to the Java Language Specification that you posted, which point do you think gets executed in the case of the question above? The last one (since I'm still trying to understand capture conversion and lub(T1,T2))?? Also, Is it really possible to apply boxing to a null value? Wouldn't this be like "guessing"??Stenophyllous
´@Gevorg A null pointer is a valid pointer to every possible object so nothing bad can happen there. The compiler just assumes that null is an Integer which it then can autobox to int.Singlestick
@Gevorg - See nowaq's comment and my response to his post. I think he picked the correct clause. lub(T1,T2) is the most specific reference type in common in the type hierarchy of T1 and T2. (They both share at least Object, so there is always a most specific reference type.)Monro
@TedHopp I analyzed lub(T1,T2) a lot (see my comments all around and my answer below). I thought the answer was in there as well but I changed my mind since I do not believe that null can be boxed to anything. Explicit boxing of null to Integer -new Integer(null); would throw a NumberFormatException and this does not happen.Stenophyllous
@Gevorg - null is not boxed into an Integer, it is interpreted as a reference to an Integer (a null reference, but that's not a problem). No Integer object is constructed from the null, so there's no reason for a NumberFormatException.Monro
@TedHopp I see you're point of view but looking at the JLS that you posted: Let T1 be the type that results from applying boxing conversion to S1 still makes me think that the JVM applies boxing to null. Maybe this is not the case to look at though..Stenophyllous
@Gevorg - If you look at the rules for boxing conversion, and apply them to null (which is not a primitive numeric type), the applicable clause is "If p is a value of any other type, boxing conversion is equivalent to an identity conversion". So boxing conversion of null to Integer yields null, without invoking any Integer constructor.Monro
It seems to me that what is actually happening here is that the compiler only goes as far as the boxing conversion of both, to get a null Int and a zero Int. But unboxing a null throws a null-pointer exception, so when it tries to unbox the null Int at run time, it gets that error. This is probably what you are saying. Saying "autoboxing/unboxing" makes it sound like both happened at compile here, but I think you were just referring to the rules in general which include both things, but only boxing happens for the code above.Caldwell
@Anon316 - Actually, both the boxing and unboxing conversions happen at run time. For the ternary operator, the compiler either pushes null to the stack directly or else pushes 0 and then calls the static method Integer.valueOf() (which replaces the top element on the stack with a reference to an Integer object). It then generates bytecode to obtain the return value via an instance method call to Integer.intValue() using the presumed Integer object reference on the stack. The NPE is triggered when the runtime discovers that the object reference is null.Monro
@TedHopp - yes, both boxing and unboxing happen at runtime, but what happens at compile time. I read the specs to say that, for this case, only boxing happens - boxing the 0 to Int, and using that to determine the result between null and Int (a reference type) is the reference type Int.Caldwell
@Anon316 - Ah, now I understand our discussion. The spec is a bit confusing because it is talking about boxing conversion of types, not of values. So when the compiler does a type analysis, it applies a boxing conversion to the null type (not to the value null) and to the primitive type int (not to the value 0). The results are the null type (the null type is its own boxing conversion) and the Integer reference type. Then the least upper bound of those two types is Integer.Monro
H
40

I think, the Java compiler interprets true ? null : 0 as an Integer expression, which can be implicitly converted to int, possibly giving NullPointerException.

For the second case, the expression null is of the special null type see, so the code return null makes type mismatch.

Haddock answered 11/11, 2011 at 19:32 Comment(11)
I assume this is related to auto-boxing? Presumably the first return would not compile prior to Java 5, right?Hjerpe
@Michael that appears to be the case if you set Eclipse's compliance level to pre-5.Loden
@Michael: this definitely looks like auto-boxing (I'm quite new to Java, and cannot make more defined statement -- sorry).Haddock
@Haddock how would the compiler end up interpreting true ? null : 0 as Integer? By autoboxing 0 first??Stenophyllous
@Gevorg: Look here: Otherwise, the second and third operands are of types S1 and S2 respectively. Let T1 be the type that results from applying boxing conversion to S1, and let T2 be the type that results from applying boxing conversion to S2. and the following text.Haddock
@Haddock I'm still trying to understand capture conversion and lub(T1,T2) and I don't understand how the compiler could try to autobox a null value to anything... Wouldn't that be random guessing to "make things work"? The alternative would be autoboxing 0 to Integer first, then we would fall into point 3 of your link, but I'm not sure that this is what's happening here..Stenophyllous
@Gevorg: I think it's autoboxing int 0 to Integer.Haddock
@Haddock right, that would be the last case of the language specification and my best guess as well. But it's hard for me to believe that the compiler would then jump to point 3 since I believe that it should check the different possibilities in order as suggested by those "if, otherwise"... lub(T1,T2) is pretty painful to understand for me..Stenophyllous
@Haddock also, if you try to explicitly box null to Integer with new Integer(null); "Let T1 be the type that results from applying boxing conversion to S1..." you would get a NumberFormatException and this is not the case.Stenophyllous
@Gevorg: well, I would use (Integer)null for explicating the intention: new Integer(null) calls the constructor Integer(String).Haddock
@Haddock that's casting and not explicit boxing. Explicit boxing is defined as using the constructor or the valueOf functionStenophyllous
A
32

Actually, its all explained in the Java Language Specification.

The type of a conditional expression is determined as follows:

  • If the second and third operands have the same type (which may be the null type), then that is the type of the conditional expression.

Therefore the "null" in your (true ? null : 0) gets an int type and then is autoboxed to Integer.

Try something like this to verify this (true ? null : null) and you will get the compiler error.

Auberge answered 11/11, 2011 at 19:50 Comment(6)
But that clause of the rules doesn't apply: the second and third operands do not have the same type.Monro
Then the answer seems to be in the following statement: > Otherwise, the second and third operands are of types S1 and S2 respectively. Let T1 be the type that results from applying boxing conversion to S1, and let T2 be the type that results from applying boxing conversion to S2. The type of the conditional expression is the result of applying capture conversion (§5.1.10) to lub(T1, T2) (§15.12.2.7).Auberge
I think that's the applicable clause. Then it tries to apply auto-unboxing in order to return an int value from the function, which causes a NPE.Monro
@Auberge I thought this too. However, if you try to explicitly box null to Integer with new Integer(null); "Let T1 be the type that results from applying boxing conversion to S1..." you would get a NumberFormatException and this is not the case...Stenophyllous
@Gevorg I'd think since an exception happens when doing the boxing we don't get ANY result here. The compiler is just obliged to generate code that follows the definition which it does - we just get the exception before we're done.Singlestick
@Singlestick - There is no exception generated when boxing anything. The exception happens when _un_boxing the null in order to return an int value from the method. If the method signature were changed to return an Integer, there would be no exception at all -- it would just return null.Monro
Z
25

In the case of the if statement, the null reference is not treated as an Integer reference because it is not participating in an expression that forces it to be interpreted as such. Therefore the error can be readily caught at compile-time because it is more clearly a type error.

As for the conditional operator, the Java Language Specification §15.25 “Conditional Operator ? :” answers this nicely in the rules for how type conversion is applied:

  • If the second and third operands have the same type (which may be the null type), then that is the type of the conditional expression.

    Does not apply because null is not int.

  • If one of the second and third operands is of type boolean and the type of the other is of type Boolean, then the type of the conditional expression is boolean.

    Does not apply because neither null nor int is boolean or Boolean.

  • If one of the second and third operands is of the null type and the type of the other is a reference type, then the type of the conditional expression is that reference type.

    Does not apply because null is of the null type, but int is not a reference type.

  • Otherwise, if the second and third operands have types that are convertible (§5.1.8) to numeric types, then there are several cases: […]

    Applies: null is treated as convertible to a numeric type, and is defined in §5.1.8 “Unboxing Conversion” to throw a NullPointerException.
Zoroastrianism answered 11/11, 2011 at 20:43 Comment(5)
If 0 is autoboxed to Integer then the compiler is executing the last case of the "ternary operator rules" as described in the Java Language Specification. If that's true, then it's hard for me to believe that it would then jump to case 3 of the same rules that is having a null and a reference type that make the return value of the ternary operator being the reference type (Integer)...Stenophyllous
@Gevorg - Why is it hard to believe that the ternary operator is returning an Integer? That's exactly what's happening; the NPE is being generated by trying to unbox the expression value in order to return an int from the function. Change the function to return an Integer and it will return null with no problem.Monro
@TedHopp: Gevorg was responding to an earlier revision of my answer, which was incorrect. You should ignore the discrepancy.Zoroastrianism
@JonPurdy "A type is said to be convertible to a numeric type if it is a numeric type, or it is a reference type that may be converted to a numeric type by unboxing conversion" and I don't think that null falls in this category. Also, we would then go into the "Otherwise, binary numeric promotion (§5.6.2) is applied ... Note that binary numeric promotion performs unboxing conversion (§5.1.8) ..." step to determine the return type. But unboxing conversion would generate a NPE and this happens only at runtime and not while trying to determine the ternary operator type. I'm still confused..Stenophyllous
@Gevorg: Unboxing happens at runtime. The null is treated as though it had type int, but is actually equivalent to throw new NullPointerException(), that’s all.Zoroastrianism
S
11

The first thing to keep in mind is that Java ternary operators have a "type", and that this is what the compiler will determine and consider no matter what the actual/real types of the second or third parameter are. Depending on several factors the ternary operator type is determined in different ways as illustrated in the Java Language Specification 15.26

In the question above we should consider the last case:

Otherwise, the second and third operands are of types S1 and S2 respectively. Let T1 be the type that results from applying boxing conversion to S1, and let T2 be the type that results from applying boxing conversion to S2. The type of the conditional expression is the result of applying capture conversion (§5.1.10) to lub(T1, T2) (§15.12.2.7).

This is by far the most complex case once you take a look at applying capture conversion (§5.1.10) and most of all at lub(T1, T2).

In plain English and after an extreme simplification we can describe the process as calculating the "Least Common Superclass" (yes, think of the LCM) of the second and third parameters. This will give us the ternary operator "type". Again, what I just said is an extreme simplification (consider classes that implement multiple common interfaces).

For example, if you try the following:

long millis = System.currentTimeMillis();
return(true ? new java.sql.Timestamp(millis) : new java.sql.Time(millis));

You'll notice that resulting type of the conditional expression is java.util.Date since it's the "Least Common Superclass" for the Timestamp/Time pair.

Since null can be autoboxed to anything, the "Least Common Superclass" is the Integer class and this will be the return type of the conditional expression (ternary operator) above. The return value will then be a null pointer of type Integer and that is what will be returned by the ternary operator.

At runtime, when the Java Virtual Machine unboxes the Integer a NullPointerException is thrown. This happens because the JVM attempts to invoke the function null.intValue(), where null is the result of autoboxing.

In my opinion (and since my opinion is not in the Java Language Specification many people will find it wrong anyway) the compiler does a poor job in evaluating the expression in your question. Given that you wrote true ? param1 : param2 the compiler should determine right away that the first parameter -null- will be returned and it should generate a compiler error. This is somewhat similar to when you write while(true){} etc... and the compiler complains about the code underneath the loop and flags it with Unreachable Statements.

Your second case is pretty straightforward and this answer is already too long... ;)

CORRECTION:

After another analysis I believe that I was wrong to say that a null value can be boxed/autoboxed to anything. Talking about the class Integer, explicit boxing consists in invoking the new Integer(...) constructor or maybe the Integer.valueOf(int i); (I found this version somewhere). The former would throw a NumberFormatException (and this does not happen) while the second would just not make sense since an int cannot be null...

Stenophyllous answered 12/11, 2011 at 6:57 Comment(1)
The null in OP's original code is not boxed. The way it works is: the compiler assumes that the null is a reference to an Integer. Using the rules for ternary expression types, it decides the entire expression is an Integer expression. It then generates code to autobox the 1 (in case the condition evaluates to false). During execution, the condition evaluates to true so the expression evaluates to null. When trying to return an int from the function, the null is unboxed. That then throws a NPE. (The compiler might optimize most of this away.)Monro
G
4

Actually, in the first case the expression can be evaluated, since the compiler knows, that it must be evaluated as an Integer, however in the second case the type of the return value (null) can not be determined, so it can not be compiled. If you cast it to Integer, the code will compile.

Gerik answered 15/11, 2011 at 20:9 Comment(0)
C
2
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
}
Covering answered 10/12, 2013 at 20:52 Comment(0)
U
0

How about this:

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);

    }

}

The output is true, true.

Eclipse color codes the 1 in the conditional expression as autoboxed.

My guess is the compiler is seeing the return type of the expression as Object.

Ulita answered 8/9, 2016 at 19:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.