Как преобразовать список List в массив T[] при помощи обобщённого метода

Мой код:

public static <T> T[] getArray(List<T> list){
    return (T[]) list.toArray(); 
}

При вызове этого метода выбрасывается исключение:

List<Integer> list = Arrays.asList(1, 2, 3, 4);
Integer[] arr = getArray(list); //Возникает исключение ClassCastException

Возможно ли преобразовать из List<T> в T[]?

Тут не получится создать цикл из-за того что нельзя создать массив дженериков, например: T[] array = new T[5]; так как код не компилируемый.


Ответы (2 шт):

Автор решения: Nowhere Man

Представленная в вопросе реализация метода getArray использует метод List::toArray, который всегда возвращает массив объектов Object[], поэтому приведение типа для этого массива бессмысленно и будет всегда выбрасываться исключение:

List<Integer> list = Arrays.asList(1, 2, 3, 4);
Integer[] arr = (Integer[]) getArray(list);  // java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Integer;...

Поэтому нужно использовать метод, возвращающий массив корректного типа List::toArray(T[] arr), в который следует передать массив известного типа, причём рекомендуется, чтобы этот массив имел нулевой размер (см. пост от Алексея Шипилёва (2016)).

Однако при этом возникает вопрос создания массива нужного типа T в нашем обобщённом методе getArray, чтобы передать его в метод List::toArray(T[] arr) (см. Generic и массивы (2013)).
Для создания массива следует использовать метод java.lang.reflect.Array.newInstance(Class<?> componentType, int size), который требует в качестве входного параметра информацию о классе, к которому относятся элементы массива.

Проще всего передать информацию о классе в качестве параметра в исходный метод:

@SuppressWarnings("unchecked")
public static <T> T[] getArray(List<? super T> list, Class<? super T> clazz) {
    return list.toArray((T[]) Array.newInstance(clazz, 0));
}

// использование
List<Integer> list = Arrays.asList(1, 2, 3, 4);
Integer[] arr = getArray(list, Integer.class);  // всё работает

Такая реализация позволяет устанавливать нужный класс для элементов массива, который может быть и классом-родителем (предком) для элементов массива:

List<Integer> list = Arrays.asList(1, 2, 3, 4);
Number[] arrNum = getArray(list, Number.class);  // всё работает
arrNum[0] = 1.5;
arrNum[1] = 0.5f;

Object[] objs = getArray(List.of(""), Object.class); 
objs[0] = new Object();

List<Double> listDouble = Arrays.asList();
Double[] doubles = getArray(listDouble, Double.class); // пустой массив Double

Также данный способ подходит для пустого списка или когда тип результирующего массива является родительским классом для элементов в списке (List<Child> -> Parent[]), так как информацию о типе элементов проще предоставить явно через дополнительный параметр Class<T> в методе getArray.

Допустим, есть иерархия:

class Parent {
    @Override
    public String toString() {
        return this.getClass().toString();
    }
}

class Child extends Parent {}
class Grand extends Child {}

Тогда список, содержащий разные элементы: var foo = List.of(new Child(), new Grand(), new Parent()); можно будет преобразовать только в массив "родителей", указав явно тип массива-результата:

var allTypes = List.of(new Child(), new Grand(), new Parent());
Parent[] foo = getArray(allTypes, Parent.class); // наиболее общий тип для успешной конвертации

var children = List.of(new Child(), new Grand(), new Grand());
Parent[] bar = getArray(children, Parent.class); // тип для успешной записи "родителя"
bar[0] = new Parent();

Однако, если входной список непустой и однородный (содержит хотя бы один ненулловый элемент одного класса), и его требуется преобразовать в массив элементов того же класса, то есть, List<T> -> T[], то будет достаточно извлечь информацию о классе из первого подходящего элемента:

private static <T> Class<?> findItemClass(List<T> list) {
    return list.stream()
               .filter(Objects::nonNull)
               .findFirst()
               .map(Object::getClass)
               .orElse(Object.class);
}

@SuppressWarnings("unchecked")
public static <T> T[] sameTypeArray(List<T> list) {
    Class<?> clazz = findItemClass(list);
    return list.toArray((T[]) Array.newInstance(clazz, 0));
}

// использование
List<Integer> list = Arrays.asList(null, 1, 2, 3, 4, null);
Integer[] arr = sameTypeArray(list);  // всё работает

Разумеется, результат такого метода может быть присвоен массиву родительского типа, так как массивы ковариантны, но при попытке записать в этот массив некорректное значение будет выброшено исключение ArrayStoreException:

Number[] arrInt = sameTypeArray(list);  // ок, тип массива [Ljava.lang.Integer
arrInt[0] = 1.5; // ArrayStoreException, попытка записи Double в Integer[]

Object[] arrObj = sameTypeArray(List.of("")); // [Ljava.lang.String
arrObj[0] = new Object(); // ArrayStoreException, попытка записи Object в String[] 

При необходимости, метод findItemClass можно улучшить, чтобы искать наиболее общий родительский класс для ненулловых элементов списка, например, так:

static Class<?> commonSuperclass(Class<?> a, Class<?> b) {
    while (!a.isAssignableFrom(b))
        a = a.getSuperclass();
    return a;
}

static <T> Class<?> findItemClass(List<? extends T> list) {
    return list.stream()
        .filter(Objects::nonNull)
        .map(Object::getClass)
        .reduce(MyClass::commonSuperclass) // ссылка на метод выше
        .orElse(Object.class);
}

И с такой реализацией можно будет преобразовывать список для различных элементов:

var family = Arrays.asList(null, new Child(), new Grand(), new Parent());
var arrFamily = getArray(family); // [LParent

var kids = Arrays.asList(null, new Child(), new Grand(), null);
var arrKids = getArray(kids); // [LChild
→ Ссылка
Автор решения: talex moved to Codidact

Рабочее но некрасивое решение.

abstract class ArrayFactory<T> {
    private final List<T> list;

    public ArrayFactory(List<T> list) {
        this.list = list;
    }

    @SneakyThrows
    public T[] toArray() {
        Type arg = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        Class<?> clazz = getClass().getClassLoader().loadClass(arg.getTypeName());
        return list.toArray((T[]) Array.newInstance(clazz, list.size()));
    }
}

Использовать так:

    Parent[] array = new ArrayFactory<>(List.of(new Child(), new Parent())) {}.toArray();

Альтернатива - переход на Scala.

→ Ссылка