How to convert List to Array t[] (for primitive

2019-02-25 06:07发布

问题:

I am doing some tests with generic-methods and I'd like to transform these two methods below (convertFloatListToArray and convertShortListToArray) in just one (convertListToArray):

public class Helper{
    public static float[] convertFloatListToArray(List<Float> list){
        float[] array = new float[list.size()];

        for(int i = 0; i<list.size(); i++){
            array[i] = list.get(i);
        }

        return array;
    }

    public static short[] convertShortListToArray(List<Short> list){
        short[] array = new short[list.size()];

        for(int i = 0; i<list.size(); i++){
            array[i] = list.get(i);
        }

        return array;
    }
}

But when I try to use generics, as below, I have some errors:

public class Helper{
    public static <T, E> T convertListToArray(List<E> list){
        T array = new T[list.size()];

        for(int i = 0; i<list.size(); i++){
            array[i] = list.get(i);
        }

        return array;
    }
}

I can understand java limitations about generics, but I wonder if someone know any solution, using generic-method, that I am not seeing.

回答1:

As of the current version (Java 12), primitive types can't be represented with Java generics. More specifically, we can't provide a primitive type as a type argument. (We can't do e.g. Foo<int>.) We also can't use type variables as the type in a new expression, so we can't do new T[n] to create an array. Therefore, there's no ideal way to do this.

It is possible to do this reasonably using some reflection (java.lang.reflect.Array), but we need to provide a Class as an argument. Here's an example of how it might be done:

/**
 * Unboxes a List in to a primitive array.
 *
 * @param  list      the List to convert to a primitive array
 * @param  arrayType the primitive array type to convert to
 * @param  <P>       the primitive array type to convert to
 * @return an array of P with the elements of the specified List
 * @throws NullPointerException
 *         if either of the arguments are null, or if any of the elements
 *         of the List are null
 * @throws IllegalArgumentException
 *         if the specified Class does not represent an array type, if
 *         the component type of the specified Class is not a primitive
 *         type, or if the elements of the specified List can not be
 *         stored in an array of type P
 */
public static <P> P toPrimitiveArray(List<?> list, Class<P> arrayType) {
    if (!arrayType.isArray()) {
        throw new IllegalArgumentException(arrayType.toString());
    }
    Class<?> primitiveType = arrayType.getComponentType();
    if (!primitiveType.isPrimitive()) {
        throw new IllegalArgumentException(primitiveType.toString());
    }

    P array = arrayType.cast(Array.newInstance(primitiveType, list.size()));

    for (int i = 0; i < list.size(); i++) {
        Array.set(array, i, list.get(i));
    }

    return array;
}

Example call:

List<Integer> list = List.of(1, 2, 3);
int[] ints = toPrimitiveArray(list, int[].class);

Note that Array.set will perform a widening primitive conversion, so the following works:

List<Integer> list = List.of(1, 2, 3);
double[] doubles = toPrimitiveArray(list, double[].class);

But it won't perform a narrowing conversion, so the following throws an exception:

List<Integer> list = List.of(1, 2, 3);
byte[] bytes = toPrimitiveArray(list, byte[].class); // throws

If you wanted, that code could also be used to make duplication easier:

public static int[] toIntArray(List<Integer> list) {
    return toPrimitiveArray(list, int[].class);
}
public static double[] toDoubleArray(List<Double> list) {
    return toPrimitiveArray(list, double[].class);
}
...

(Having multiple methods like that isn't really generic, though.)


One solution that you'll sometimes see places looks something like this:

public static <P> P toPrimitiveArray(List<?> list) {
    Object obj0 = list.get(0);
    Class<?> type;
    // "unbox" the Class of obj0
    if (obj0 instanceof Integer)
        type = int.class;
    else if (obj0 instanceof Double)
        type = double.class;
    else if (...)
        type = ...;
    else
        throw new IllegalArgumentException();

    Object array = Array.newInstance(type, list.size());

    for (int i = 0; i < list.size(); i++) {
        Array.set(array, i, list.get(i));
    }

    return (P) array;
}

There are a variety of problems with that, though:

  • We don't know what type of array to create if the list is empty.
  • Doesn't work if there's more than one type of object in the list.
  • Unchecked casting of the result array to P, so there's a danger of heap pollution.

It's much better to just pass in a Class as an argument.


Also, while it's possible to just write many overloads which unbox arrays:

public static int[]    unbox(Integer[] arr) {...}
public static long[]   unbox(Long[]    arr) {...}
public static double[] unbox(Double[]  arr) {...}
...

Because of the effects of type erasure, it's impossible to write overloads which unbox many different types of List, as in the following:

public static int[]    unbox(List<Integer> list) {...}
public static long[]   unbox(List<Long>    list) {...}
public static double[] unbox(List<Double>  list) {...}
...

That won't compile, because we aren't allowed to have more than one method in the same class with the same name and erasure. The methods would have to have different names.


As a side-note, here are some non-generic solutions:

  • As of Java 8 we can unbox Lists of Integer, Long and Double using the Stream API:

    List<Long> list = List.of(1L, 2L, 3L);
    long[] longs = list.stream().mapToLong(Long::longValue).toArray();
    
  • Google Guava has Collection unboxing methods in their com.google.common.primitives classes, for example Doubles.toArray:

    List<Double> list = List.of(1.0, 2.0, 3.0);
    double[] doubles = Doubles.toArray(list);