Chapter 4. Autoboxing and Unboxing
When you begin to study Java, one of the first lessons is always about
objects. In fact, you could say that java.lang.Object
is the very cornerstone
of Java. Practically 99% of everything you do in the language
revolves around that class, or one of its subclasses. It’s the 1% of the
time, though, that can be a pain—when you suddenly find yourself having
to convert between your objects and Java primitives.
Primitives in Java are your ints, shorts, chars
, and so on—types that
aren’t objects at all. As a result,
Java has wrapper classes, such as
Integer, Short
, and Character
, which are object versions of the primitive
types. Where things get annoying is when you have to go back and
forth between the two—converting a primitive to its wrapper, using it,
then converting the object’s value back to a primitive. Suddenly, methods
such as intValue( )
begin to litter your code.
Happily, Tiger finally takes care of this issue, at least as much as can be expected without tossing out primitives completely. This is handled through two new conversion features: boxing and unboxing. And, just to add some more words to the English language, it does these conversions automatically, so we now talk about autoboxing and auto-unboxing.
Converting Primitives to Wrapper Types
Literal values in Java are always
primitives. The number 0
, for example,
is an int
, and must be converted to an object through code like this:
Integer i = new Integer(0);
This is pretty silly, for obvious reasons, and Tiger removes the need for such nonsense through boxing.
How do I do that?
You can now dispense with the manual conversions, and let the Java virtual machine (VM) handle conversion of primitives to object wrapper types:
Integer i = 0;
In the background, Java handles taking this primitive and turning it into a wrapper type. The same conversion happens with explicit primitive types:
int foo = 0; Integer integer = foo;
If you’re not completely convinced of the value of this, try typing these statements into a pre-Tiger compiler, and watch in amazement as you get some rather ridiculous errors:
compile-1.4: [echo] Compiling all Java files... [javac] Compiling 1 source file to classes [javac] src\com\oreilly\tiger\ch04\ConversionTester.java:6: incompatible types [javac] found : int [javac] required: java.lang.Integer [javac] Integer i = 0; [javac] ^ [javac] src\com\oreilly\tiger\ch04\ConversionTester.java:9: incompatible types [javac] found : int [javac] required: java.lang.Integer [javac] Integer integer = foo; [javac] ^ [javac] 2 errors
These errors “magically” disappear in Tiger when using the - source 1.5
switch.
What just happened?
Behind the scenes, these
primitive values are boxed. Boxing refers to the
conversion from a primitive to its corresponding wrapper type: Boolean
,
Byte, Short, Character, Integer, Long, Float
, or Double
. Because this
happens automatically, it’s generally referred to as autoboxing.
It’s also common for Java to perform a widening conversion in addition to boxing a value:
Number n = 0.0f;
Here, the literal is boxed into a Float
, and then widened into a Number
.
Note
It’s possible to specifically request a boxing conversion—that’s basically a new form of casting. It’s just easier to let the VM handle it, though.
Additionally, the Java specification indicates that certain primitives are
always to be boxed into the same immutable wrapper objects. These
objects are then cached and reused, with the expectation that these are
commonly used objects. These special values are the boolean
values true
and false
, all byte
values, short and int
values between -128 and 127,
and any char
in the range \u0000
to \u007F
. As this all happens behind
the scenes, it’s more of an implementation detail than something you
need to worry much about.
Converting Wrapper Types to Primitives
Just as Tiger converts primitives to wrapper types as needed, the reverse is also true. Like boxing, unboxing involves little effort on the part of the programmer.
How do I do that?
Here’s some more simple code that does both boxing and unboxing, all without any special instruction:
// Boxing int foo = 0; Integer integer = foo; // Simple Unboxing int bar = integer; Integer counter = 1; // boxing int counter2 = counter; // unboxing
Pretty simple, isn’t it?
What about...
...null
value assignment? Since null
is a legal value for an object, and
therefore any wrapper type, the following code is legal:
Integer i = null; int j = i;
i
is assigned null
(which is legal), and then i
is unboxed into j
. However,
null
isn’t a legal value for a primitive, so this code throws a
NullPointerException
.
Incrementing and Decrementing Wrapper Types
When you begin to think about the
implications of boxing and unboxing,
you’ll realize that they are far-reaching. Suddenly, every operation available
to a primitive should be available to its wrapper-type counterpart,
and vice versa. One of the immediate applications is the increment and
decrement operations: ++
and --
. Both of these operations now work for
wrapper types.
How do I do that?
Well, without much work, actually:
Integer counter = 1; while (true) { System.out.printf("Iteration %d%n", counter++); if (counter > 1000) break; }
The variable counter
is treated just as an int
in this code.
What just happened?
It’s worth noting that more happened here than perhaps meets the eye. Take this simple portion of the example code:
counter++
Remember that counter
is an Integer
. So the value in counter
was first
auto-unboxed into an int
, as that’s the type required for the ++
operator.
Tip
This is actually an important point—
the ++
operator has not been changed to work with object wrapper types—
it’s only through autounboxing that this code works.
Once the value is unboxed, it is incremented. Then, the new value has to
be stored back in counter
, which requires a boxing operation. All this in
a fraction of a second!
You might also notice that the Integer
value of counter
was compared to
the literal, and therefore primitive, value 1000
. This is just another example
of autounboxing at work.
Boolean Versus boolean
The boolean
type is a little bit of a special case for Java primitives, mostly
because it has several logical operators associated with it, such as!
(not),
||
(or), and &&
(and). With unboxing, these are now useful for Boolean
values as well.
How do I do that?
Any time you have an expression that uses !
, ||
, or &&
, any Boolean
values
are unboxed to boolean
primitive values, and evaluated accordingly:
Boolean case1 = true; Boolean case2 = true; boolean case3 = false; Boolean result = (case1 || case2) && case3;
In this case, the result of the expression, a boolean
, is boxed into the
result
variable.
What about...
...direct object comparison? Object comparison works as it always has:
Integer i1 = 256; Integer i2 = 256; if (i1 == i2) System.out.println("Equal!"); else System.out.println("Not equal!");
The result of running this code, at least in my JVM, is the text “Not
equal!” In this case, there is not an unboxing operation involved. The literal
256
is boxed into two different Integer
objects (again, in my JVM),
and then those objects are compared with ==
. The result is false, as the
two objects are different instances, with different memory addresses.
Because both sides of the ==
expression contain objects, no unboxing
occurs.
Warning
You can’t depend on this result; it’s merely used as an illustration.
Some JVMs may choose to try and optimize this code, and create
one instance for both Integer
objects, and in that case, the ==
operator would return a true result.
But, watch out! Remember (from “Converting Primitives to Wrapper Types”), that certain primitive values are unboxed into constant, immutable wrapper objects. So, the result of running the following code might be surprising to you:
Integer i1 = 100; Integer i2 = 100; if (i1 == i2) System.out.println("Equal!"); else System.out.println("Not equal!");
Here, you would get the text “Equal!” Remember that int
values from -127
to 127 are in that range of immutable wrapper types, so the VM actually
uses the same object instance (and therefore memory address) for both i1
and i2
. As a result, ==
returns a true result. You have to watch out for this,
as it can result in some very tricky, hard-to-find bugs.
Conditionals and Unboxing
One of the odder features of Java
is the conditional operator, often called
the ternary operator. This is the operator version of an if/else
statement,
represented by the ?
character. Since it evaluates an expression,
the unboxing features of Tiger affect it, too. You can use it with all sorts
of new types.
How do I do that?
Here is the format of this operator:
[conditional expression] ? [expression1][expression2]
If [conditional expression]
evaluates to true
, then [expression1]
is
executed; otherwise [expression2]
is. In pre-Tiger Java,
[conditional expression]
had to result in a boolean
value. This was a bit of a pain if
you had a method that returned a Boolean
wrapper type, or an expression
that involved a Boolean
. In Tiger, this is no longer a problem, and
the ternary operator happily gobbles up any unboxed Boolean
values:
Boolean arriving = false; Boolean late = true; System.out.println(arriving ? (late ? "It's about time!" : "Hello!") : (late ? "Better hurry!" : "Goodbye"));
What just happened?
The ternary operator is a little tricky, in both Java 1.4 and Tiger, so it’s
worth mentioning some additional details. In pre-Tiger environments,
[expression1]
and [expression2]
had to either be of the same type, or
one had to be assignable to the other. So both had to be String
values,
or one could be an int
and the other a float
(as an int
could be widened
to a float
). In Tiger, the restrictions loosen a bit due to unboxing.
One or both expressions can be unboxed, so one could be an Integer
and the other could be a Float
, for example. However, both will be
unboxed, and the int
will be widened to float
, so the return type of the
expression would be a float
–the result is not boxed back into a Float
.
Another addition to Tiger is automatic casting of reference to their intersection type. That’s a mouthful, so here’s an example:
String s = "hello"; StringBuffer sb = new StringBuffer("world"); boolean mutable = true; CharSequence cs = mutable ? sb : s;
In pre-Tiger environments, this would generate an error, as sb
(a
StringBuffer
) and s
(a String
) cannot be assigned to each other. However,
this code should really work, as both String
and StringBuffer
implement the CharSequence
interface. However, you have to perform
some casting:
CharSequence cs = mutable ? (CharSequence)sb : (CharSequence)s;
In Tiger, though, any valid intersection of the two operands can be used.
This is essentially any object, walking up the inheritance chain, that is
common to both operands. In this case, CharSequence
fits that criteria,
and so is a valid return type.
Note
Technically, this is a feature of the generic support in Tiger, but it seemed appropriate to mention it here. Generics are covered in detail in Chapter 2.
As a side effect of this, note that two reference types (objects) always
share java.lang.Object
as a common ancestor, so any result of a ternary
operation involving non-primitive operands can be assigned to
java.lang.Object
.
Control Statements and Unboxing
There are several control statements
in Java that take as an argument a
boolean
value, or an expression that results to a boolean
value. It
shouldn’t be much of a surprise that these expressions now also take Boolean
values. Additionally, the switch
statement has an array of new
types it will accept.
How do I do that?
if/else, while
, and do
all are affected by Tiger’s ability to unbox Boolean
values to boolean
values. By now, this shouldn’t require much explanation:
Boolean arriving = false; Boolean late = true; Integer peopleInRoom = 0; int maxCapacity = 100; boolean timeToLeave = false; while (peopleInRoom < maxCapacity) { if (arriving) { System.out.println("It's good to see you."); peopleInRoom++; } else { peopleInRoom--; } if (timeToLeave) { do { System.out.printf("Hey, person %d, get out!%n", peopleInRoom); peopleInRoom--; } while (peopleInRoom > 0); } }
There are several boxing and unboxing operations going on here, in several control statements. Browse through this code, and work mentally through each operation.
Another statement that benefits from unboxing is switch
. In pre-Tiger
JVMs, the switch
statement accepts int, short, char
, or byte
values.
With unboxing in play, you can now supply it with Integer, Short, Char
,
and Byte
values as well, in addition to the introduction of enums
.
Note
Enums are covered in Chapter 3.
Method Overload Resolution
Boxing and unboxing offer a lot of solutions to common problems (or, at least annoyances) in Java programming. However, these solutions manage to introduce a few quirks of their own, particularly in the area of method resolution. Method resolution is the process by which the Java compiler determines which method is being invoked. You’ll need to be careful, as unboxing and boxing affect this process.
How do I do that?
In the normal case, Java handles method resolution by using the name of the method. In cases where a method is overloaded, though, an extra step must be taken. The arguments to the method are examined, and matched up with the arguments that a specific version of the requested method accepts. If no matching argument list is found, you get a compiler error. Sounds simple enough, right? Well, consider the following two methods:
public void doSomething(double num);
public void doSomething(Integer num);
Now supposed that you invoked doSomething( )
:
int foo = 1; doSomething(foo);
Which method is called? In a pre-Tiger environment, this is easy to
determine. The int
is widened to a double
, and doSomething(double num)
is called. However, in a Tiger environment, it would seem that boxing
would occur, and doSomething(Integer num)
would be what the
method invocation would resolve to. While that’s reasonable, it is not
what happens.
Imagine writing a program like this, compiling and testing it in Java 1.4, and then recompiling it under Tiger. Suddenly, things start going haywire! Obviously, this isn’t acceptable. For that reason, method resolution in Tiger will always select the same method that would have been selected in Java 1.4. As a rule, you really shouldn’t mess around with this sort of overloading anyway, if at all possible. Be as specific as possible in your method naming and argument lists, and this issue goes away.
What just happened?
In Tiger, because of these restrictions, method resolution is a three-pass process:
The compiler attempts to locate the correct method without any boxing, unboxing, or vararg invocations. This will find any method that would have been invoked under Java 1.4 rules.
If the first pass fails, the compiler tries method resolution again, this time allowing boxing and unboxing conversions. Methods with varargs are not considered in this pass.
If the second pass fails, the compiler tries method resolution one last time, allowing boxing and unboxing, and also considers vararg methods.
These rules ensure that consistency with pre-Tiger environments is maintained.
Note
Varargs are detailed in Chapter 5.
Get Java 5.0 Tiger: A Developer's Notebook now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.