Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I pass the contents of a list to a varargs method?

I have a method that uses the varargs feature:

void add(Animal ...);

Now, instead of doing .add(dog, cat), I have an Animal list with unknown number of elements,

List<Animal> i = new ArrayList<Animal>();
i.add(dog);
i.add(cat);

and want to call add with the elements of this list.

I think I could use an array, but when I do .add(i.toArray()), it gives a compiler error.

What is the proper way to do it?

like image 713
superfish Avatar asked Sep 02 '11 18:09

superfish


1 Answers

It's:

add(i.toArray(new Animal[i.size()]))

List.toArray returns an Object[], regardless of the type argument on the List: even though you might write new List<String>().toArray(), you will get an Object[]. However, the version of toArray that takes an array to fill returns an array with the correct type: if you write new List<String>().toArray(new String[0]), you will get an String[]. Note that the size of the array you pass in doesn't even have to match the size of the list, although it's good practice to ensure that it does.

This is ultimately due to a mildly tricky feature of generics. At first glance, you might think that String[] and List<String>mean similar things for their base types - one is an array of strings, the other is a list of strings.

However, they are in fact very different.

An array is a language primitive, and has its type baked into it. If you took a hex editor and looked at an array instance in memory in the JVM, you would be able to find (somewhere nearby) a record of the type of objects it holds. That means that if you take an instance of an array of some unknown component type, you can find out what that type is. Conversely, it means that if you're going to create an instance of an array, you need to know what component type you want.

The List, on the other hand, uses generics, which in Java is implemented with type erasure, which means that, roughly speaking, it is something that exists in the compiler, but not at runtime (the compiler can check that you get it right, but the JVM can't). This leads to a simple and efficient implementation (one simple enough to have been added to pre-generics Java without changing the JVM), but it has some shortcomings - in particular, that at runtime, there is no way to tell what the type argument on any particular instance of a generic class is, because type arguments only exist in the compiler. Because it is up to the List instance to handle toArray(), the only thing it can do is create an Object[]. It just doesn't know of a more specific type to use.

One way of looking at this is that arrays have a type argument as part of their class, whereas Lists have a type argument as part of their type, and since objects have classes but variables have types, you can't get the type argument of a List from an object, only from a variable holding an object (as an aside, you also can't get the type argument of an array from a variable holding an array (consider Object[] array = new String[0];), but that doesn't really matter because, the variable lets you get hold of an object - unless it's null).

To boil this down to code, the problem is:

public <E> E[] createSimilarlyTypedArray(List<E> list) {
    Class<E> componentType = list.???; // there is no way to do this
    return Arrays.newInstance(componentType, list.size());
}
like image 74
Tom Anderson Avatar answered Oct 11 '22 15:10

Tom Anderson