How to Break Java's Type Safety System

27 Jun 2022

Take a look at the following:

Java
1
2
int[] numbers = new int[]{1, 2, 3, 4, 5};
List<Integer> numberList = new ArrayList(Arrays.asList(numbers));

This compiles and executes. It results in a list containing one entry:

Java
1
int size = numberList.size(); // size = 1

That one entry is an int[] - our original numbers primitive int array.

You can create a List<int[]> in Java - but that is not the type we specified. We specified List<Integer>. What happened? How did we manage to place an int[] into a list of type Integer?

What’s worse, we don’t find out about this until we try to access the list - for example:

Java
1
2
3
for (Integer i : numberList) {
    // we don't get this far...
}

The attempt to access numberList throws a runtime exception:

ClassCastException: class [I cannot be cast to class java.lang.Integer

This confirms that the contents of the list are not what we expected - an array of ints ([I) instead of a list of Integer objects.

Well, my IDE did flag new ArrayList(Arrays.asList(numbers)) with a warning:

Confusing primitive array passed to varargs method

And:

A primitive array passed to variable-argument method will not be unwrapped and its items will not be seen as items of the variable-length argument in the called method. Instead, the array will be passed as a single item.

The vararg in this case is the argument passed to Arrays.asList(T... a).

It turns out this issue is covered eloquently in Josh Bloch’s Effective Java (Third Edition, in my case). Item 32: “Combine generics and varargs judiciously” states:

when you invoke a varargs method, an array is created to hold the varargs parameters…

He goes on to ask “why is it even legal to do this…?” Because the usefulness outweighs the downside of this inconsistency.


One alternative approach these days is to use a stream to box the array of primitive int values:

Java
1
List<Integer> numberList = Arrays.stream(numbers).boxed().toList();