Как преобразовать список 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 шт):
Представленная в вопросе реализация метода 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
Рабочее но некрасивое решение.
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.