Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compiler dropping my type conversion?

I'm puzzled by what I had to do to get this code to work. It seems as if the compiler optimized away a type conversion that I needed, or there's something else I don't understand here.

I have various objects that are stored in the database that implement the interface Foo. I have an object, bar, which holds data I'm using to retrieve my Foo objects. bar has these methods:

Class getFooClass()

Long getFooId()

I pass the class and ID to a method with this signature, which delegates to hibernate which retrieves the subject based on its class and ID:

public <T> T get(Class<T> clazz, Serializable id);

There are different implementers of Foo, and some of these hibernate objects have a Long id, and others have an Integer id. Although this method accepts either, farther down it had better have the right one. So when I tried to call get() on an object with an Integer id, as follows, I understandably got an error complaining that I had provided a Long where an Integer was required:

    get(bar.getFooClass(), bar.getFooId());

There's no hibernate problem here, I just need to provide an Integer where an Integer id is required and a Long where a Long id is required. So I added a method to bar, hasLongId(), and tried this: (at this point you may be thinking this isn't a good design, but that's not my question right now)

get(bar.getFooClass(),
    bar.hasLongId() ? bar.getFooId() : bar.getFooId().intValue());

And it still complained that I had provided a Long. That seemed strange. Then I tried this:

get(bar.getFooClass(),
    bar.hasLongId() ? bar.getFooId() 
                    : new Integer(bar.getFooId().intValue()));

Same error! How can this be? So I stepped through in the debugger, and yes, it stepped through intValue() and also through the Integer constructor, but then in the get method, the passed parameter was in fact a Long—the same Long object that was returned from getFooId().

I don't understand what's happening, so I just try various things:

Integer intId = bar.getFooId().intValue();
get(bar.getFooClass(), bar.hasLongId() ? bar.getFooId() : intId);   
// same error

and

Serializable id = bar.hasLongId() ? bar.getFooId() 
                                : new Integer(bar.getFooId().intValue());
get(bar.getFooClass(), id); 
// same error

And finally:

Serializable id;
if (bar.hasLongId()) {
    id = bar.getFooId();
} else {
    id = bar.getFooId().intValue();
}
get(bar.getFooClass(), id);

This one works. So apparently it has something to do with the ternary operator. But why? Can someone explain what happened here?

like image 623
Dana Avatar asked Sep 21 '16 00:09

Dana


1 Answers

This is a great question and goes into the nitty gritty details of the semantics of the ternary expression. No, your compiler is not broken or playing tricks on you.

In this case, if the types of the second and third operands of the ternary expression is long and int, then the resulting type is always long. This is due to binary numeric promotion.

According to the JLS (Java Language Specification):

..., binary numeric promotion is applied to the operand types, and the type of the conditional expression is the promoted type of the second and third operands.

The values are getting unboxed due to rule #1 of Binary Numeric Promotion:

If any operand is of a reference type, it is subjected to unboxing conversion

What this means essentially, that when you have a ternary expression, the resulting type of the expression must be determinable statically (at compile time). The second and third operands must be coerced into a single type, which is the type of the expression. If both operands are a numeric type, binary numeric promotion kicks in to determine the final type of the expression.

like image 121
Andrew Eisenberg Avatar answered Oct 11 '22 12:10

Andrew Eisenberg