As we discussed in the previous section, one of the limitations of the
ArrayList class (which is actually a limitation of Javaβs generic types more generally) is that the element type of an
ArrayList must be a reference type. So we can make an
ArrayList<String> but not an
ArrayList<int>. So what are we supposed to do if we want to make a growable, shrinkable collection of
int or
double values?
Javaβs solution to this problem is to define
wrapper classes, classes that exist to provide a reference type that wraps up a single value of a given primitive type. These types are defined in the
java.lang pacakge and have names that are the capitalized, unabbreviated names of the corresponding primitive types:
Integer,
Double, and
Boolean.
As reference types, these types can be used as the element type of an
ArrayList:
ArrayList<Integer>,
ArrayList<Double>, and
ArrayList<Boolean>. As weβll see Java provides a bit of extra support for these types, automatically converting between wrapper types and the corresponding primitive values in many contexts.
There are also a few
static utility methods and constants defined in the wrapper classes related to the corresponding primitive type.
Subsection 10.2.1 Using wrapper classes with ArrayList
The main use of wrapper types is as type parameter in an
ArrayList declaration. If what we really want is a list of
int values we can declare an
ArrayList<Integer>.
Once weβve declared an
ArrayList with a wrapper class for an element type, how do we create instances of the wrapper class to put in it?
There are three ways, but in fact we almost only ever need one of them. The oldest, and worst, way is to construct a new instance of the wrapper class with a the constructor that takes a single value of the corresponding primitive type. These constructors exist but have been
deprecated which means they still exist because removing them would break old code but they are now considered a mistake.
ArrayList<Integer> integers = new ArrayList<>();
ArrayList<Double> doubles = new ArrayList<>();
// BAD: don't do this. But it does work
integers.add(new Integer(42));
doubles.add(new Double(3.14));
Slightly better is to use the
static utility methods,
valueOf, defined in the wrapper classes that take a primitive value and return a wrapped instance.
// Slightly better: also works. But also not necessary as we'll see
integers.add(Integer.valueOf(42));
doubles.add(Double.valueOf(3.14));
The advantage of the
valueOf methods compared to the constructors is that the methods donβt have to create a new object each time. This can save a lot of memory by reusing the same object for the same commonly wrapped primitive value. For example, making an array list containing a million zeros obtained with with
Integer.valueOf(0) would use only one object to represent all the zeros whereas if we made them with
new Integer(0) Java would have to allocate a million different objects, each holding the same
0 value, using a million times as much memory.
But the right way to do this is to ignore the wrapper types and let a feature of Java called
autoboxing take care of it for us.
// The right way
integers.add(42);
doubles.add(3.14);
Autoboxing is the automatic conversion that the Java compiler makes between primitive types and their corresponding object wrapper classes. The Java compiler applies autoboxing when a primitive value is passed as a parameter to a method that expects an object of the corresponding wrapper class or assigned to a variable of the corresponding wrapper class. So when we call
add on an
ArrayList<Integer> the compiler knows
add expects an
Integer. If it sees that weβre passing an
int it automatically gets an instance of
Integerto wrap it, basically as if we had written a call to
Integer.valueOf.
An automatic conversion going in the other direction called
unboxing happens when a value of a wrapper type is passed as a parameter or assigned to a variable that expects the corresponding primitive type. This means we can write code like this:
int i = integer.get(0);
double d = doubles.get(0);
The values actally returned by
get in those two lines are
Integer and
Double but Java unwraps them and assigns the underlying value to the primitive variables
i and
d.
Thanks to autoboxing and unboxing we almost never need to use the wrapper types anywhere except as the type parameters when declaring an
ArrayList. For the most part we can just use primitive values and everything will work out fine. Unfortunately they are a few edge cases where it doesnβt which weβll discuss below.
Activity 10.2.1.
Which of the following is the correct way to create an ArrayList of integers?
ArrayList[int] numbers = new ArrayList<>();
The square brackets [] are only used with arrays, not ArrayLists.
ArrayList<String> numbers = new ArrayList<>();
String is not the correct type since this is for an array of integers.
ArrayList<int> numbers = new ArrayList<>();
ArrayLists cannot hold primitive types like int. You must use the wrapper class Integer.
ArrayList<Integer> numbers = new ArrayList<>();
The wrapper class Integer is used to hold integers in an ArrayList.
Activity 10.2.2.
Drag the definition from the left and drop it on the correct word on the right. Click the "Check Me" button to see if you are correct.
Review the vocabulary.
- automatic conversion from the primitive type to the wrapper object
- autoboxing
- automatic conversion from the wrapper object to the primitive type
- unboxing
- Integer
- wrapper class
- int
- primitive type
- Integer.MAX_VALUE + 1
- overflow
- Integer.MIN_VALUE - 1
- underflow
Activity 10.2.3.
Hereβs an example of code that uses autoxboxing to add values to an
ArrayList<Integer>. What will print when the following code executes?
ArrayList<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
list1.add(3);
list1.add(2, 4);
list1.add(5);
System.out.println(list1);
[1, 2, 3, 4, 5]
This would be true if all the add method calls were add(value), but at least one is not.
[1, 4, 2, 3, 5]
This would be true if add(2, 4) was add(1, 4) instead
[1, 2, 4, 3, 5]
The add(2, 4) will put the 4 at index 2, but first move the 3 to index 3.
[1, 2, 4, 5]
This would be true if the add(2, 4) replaced what was at index 2, but it actually moves the value currently at index 2 to index 3.
Subsection 10.2.2 An autoboxing subtlety
As mentioned above, there are a few times when we canβt completely ignore whatβs going on with autoboxing and unboxing. If we stick strictly within the AP curriculum we wonβt hit them but we donβt have to go very far outside the bounds to run into one.
The AP curriculum covers the
ArrayList method
E remove(int) which takes an
int argument specifying the index of the element to remove. Thatβs fine. But thereβs another
remove method,
boolean remove(Object) that takes any reference type as an argument
ArrayList and either removes the first object in the list that is
equals to it and returns
true or returns
false if there was no such object to remove.
The subtlety is this. What happens if we have an
ArrayList<Integer> and then call
remove(42)? Should it call
remove(int), and remove the element at index
42 or should it autobox
42 and remove the first element of the list whose value is an
Integer wrapping the value
42?
As it happens, Java goes with
remove(int). Which means the calls to
remove below behave very differently:
ArrayList<Integer> nums = new ArrayList<>();
nums.add(10);
nums.add(20);
nums.add(30);
nums.remove(Integer.valueOf(0))
nums.remove(0);
nums.remove(Integer.valueOf(20))
nums.remove(20);
The first call to
remove does nothing and returns false because thereβs no
0 in the list. The second call removes the first element of the list,
10, because the argument is treated as the index 0, not a value. The third call successfully removes the
20 from the list. And the fourth call crashes with an
IndexOutOfBoundsException because 20, as an index, is way too big for the list which now only has one element in it.
Activity 10.2.4.
Remember that the
remove(int index) method removes a value from an
ArrayList at a specific position decreasing the size of the list by one and shifting down the items that were at higher indexes in the array by one position. It also returns the item that was removed.
What will the following code print out? Try to guess before you run it. Were you surprised? Read the note below.
Activity 10.2.5.
What will print when the following code executes?
ArrayList<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
list1.add(3);
list1.remove(2);
System.out.println(list1);
[2, 3]
This would be true if it was remove(0)
[1, 2, 3]
The remove will remove a value from the list, so this canβt be correct.
[1, 2]
The 3 (at index 2) is removed
[1, 3]
This would be true if it was remove(1)
You can step through the code above by clicking on the following
RemoveExample.
Activity 10.2.6.
What will print when the following code executes?
ArrayList<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
list1.add(3);
list1.set(2, 4);
list1.add(2, 5);
list1.add(6);
System.out.println(list1);
[1, 2, 3, 4, 5]
The set will replace the item at index 2 so this can not be right.
[1, 2, 4, 5, 6]
The add with an index of 2 and a value of 5 adds the 5 at index 2 not 3. Remember that the first index is 0.
[1, 2, 5, 4, 6]
The set will change the item at index 2 to 4. The add of 5 at index 2 will move everything else to the right and insert 5. The last add will be at the end of the list.
[1, 5, 2, 4, 6]
The add with an index of 2 and a value of 5 adds the 5 at index 2 not 1. Remember that the first index is 0.
You can step through the code above by clicking on the following
Example1.