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:

Note

The “compile-1.4” target compiles the examples from this chapter with the “-source 1.4” switch.

	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 pointthe ++ operator has not been changed to work with object wrapper typesit’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.

Note

For inquiring minds, primitives are boxed up to wrapper types in equality comparisons. For operators such as <, >=, and so forth, the wrapper types are unboxed to primitive types.

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:

Note

Thanks to Java in a Nutshell, Fifth Edition (O’Reilly) for this 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);
	  }
	}

Note

You might want to be cautious running this code—it’s actually an infinite loop.

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:

  1. 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.

  2. 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.

  3. 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.