Welcome to the fourth episode of our Java-unconfusing series, where we look at some basic concepts in Java (in fact, it applies to many other languages too) that result in confusing outcomes in certain cases. When that happens, most likely the code quality could do with some improvements. In any case, it’s good to know what we can expect from Java and never be confused by it.
Today, I would like to talk about ‘widening’…
Example
Assume we have two short
values and we’d like to calculate their sum. It would make sense to write the following piece of code:
However, this code does not compile. Java compiler considers the result of this expression as integer value instead of short
.
We could, of course, cast the result to short
, successfully compile it and get the expected result:
Why do we have to cast?
If we look at the bytecode of the previous example, we will find a following piece:
It starts with sipush
, and the function of this bytecode operation is literally to push a short
onto the stack as an integer value.
The following operation is istore_1
, and it already tells us that it stores variable a
as integer. Same happens for b
.
All the following operations are simply integer manipulations, including iadd
which sums two integer values.
At the end, because we performed the casting, we find i2s
(integer to short) casting operation, but still the final result that is
stored in the local variables is again an integer value.
But why?
What we have observed is called ‘widening primitive conversion’. The short
value, which takes two bytes, is put in a bigger ‘box’ of 4 bytes, which is an integer type.
Why is this happening in this case? There are simply no bytecode instructions which would allow to perform operations directly on short
values, so they are converted to integers.
OMG, what’s that though?
If we write our original code without any casting, but make our variables final
, there is no problem compiling that!
This behaviour is easy to explain. Let’s take a look at the relevant piece of bytecode:
As we can see, all values including the resulting sum are considered as constant values. The sum is calculated during compilation and placed directly into the bytecode. This is one of the compiler optimizations called ‘constant folding’.
In this case, compiler could calculate the sum and see that the resulting value 30000
indeed fits into 2 bytes to be of a short
type.
If we do a slight change to our code so the sum wouldn’t fit into a short
type, the compiler immediately starts to complain:
Note: if we had the same code, but all our values were integer, and the sum didn’t fit into an integer, we would have gotten a warning, but compiler would have accepted it anyway.
Cases of widening
There are different use-cases of widening primitive conversion. Often it is happening when one of the values in a single operation is of a wider type, so the other value is widened to match.
Also, short
, byte
, char
and boolean
values are always converted to int
under the hood.
There is simply no means of dealing with these types on the bytecode level.
Short doesn’t exist?
It’s a good question.
We have seen from the above examples that short
is saved into the local variables as an integer value.
So is it just a Java code sugar?
Using the following code, we can try and figure out, what is the type of our primitive variable:
The code above outputs java.lang.Short
. So did we get something wrong?
If we look in the bytecode, we will find our answers:
What is happening here is that we are saving our value as an integer variable. Then we load it onto the stack and
use Short::valueOf
method to make it short
again. Compiler knows, that it’s a short
, because we specified it in our code.
So it makes sure that our short
variables behave as they should. But in the end…
short
is a lie :)
(yes, and byte
as well)
Hope this was educative and fun!
If you liked this little post, take a look at the previous posts from the series: