Реализация списка Java <T> T[] toArray(T[] a)
Я просто смотрел на метод, определенный в интерфейсе списка: <T> T[] toArray(T[] a)
и у меня есть вопрос. Почему это общее? В связи с этим метод не является полностью безопасным для типов. Следующий фрагмент кода компилируется, но вызывает ArrayStoreException
:
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
String[] stringArray = list.toArray(new String[]{});
Мне кажется, если бы toArray не был универсальным и принимал параметр типа List, это было бы лучше.
Я написал игрушечный пример, и это нормально без общего:
package test;
import java.util.Arrays;
public class TestGenerics<E> {
private Object[] elementData = new Object[10];
private int size = 0;
public void add(E e) {
elementData[size++] = e;
}
@SuppressWarnings("unchecked")
//I took this code from ArrayList but it is not generic
public E[] toArray(E[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (E[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
public static void main(String[] args) {
TestGenerics<Integer> list = new TestGenerics<Integer>();
list.add(1);
list.add(2);
list.add(3);
//You don't have to do any casting
Integer[] n = new Integer[10];
n = list.toArray(n);
}
}
Есть ли какая-то причина, почему это так заявлено?
8 ответов
Из Javadocs:
Как и метод toArray(), этот метод действует как мост между API на основе массива и коллекции. Кроме того, этот метод обеспечивает точное управление типом времени выполнения выходного массива и может при определенных обстоятельствах использоваться для экономии затрат на выделение.
Это означает, что программист контролирует тип массива.
Например, для вашего ArrayList<Integer>
вместо Integer[]
массив вы можете захотеть Number[]
или же Object[]
массив.
Кроме того, метод также проверяет переданный массив. Если вы передаете массив, в котором достаточно места для всех элементов, toArray
Метод повторно использует этот массив. Это означает:
Integer[] myArray = new Integer[myList.size()];
myList.toArray(myArray);
или же
Integer[] myArray = myList.toArray(new Integer[myList.size()]);
имеет тот же эффект, что и
Integer[] myArray = myList.toArray(new Integer[0]);
Обратите внимание, что в более старых версиях Java последняя операция использовала отражение, чтобы проверить тип массива, а затем динамически создать массив правильного типа. Передавая массив правильного размера в первую очередь, отражение не нужно было использовать для выделения нового массива внутри toArray
метод. Это больше не так, и обе версии могут использоваться взаимозаменяемо.
Он объявляется в общем, так что вы можете написать код, такой как
Integer[] intArray = list.toArray(new Integer[0]);
без приведения массива возвращаясь.
Он объявлен со следующей аннотацией:
@SuppressWarnings("unchecked")
Другими словами, Java доверяет вам передать параметр массива того же типа, поэтому ваша ошибка не возникает.
Причина, по которой метод имеет эту подпись, заключается в том, что toArray
API предшествует дженерикам: метод
public Object[] toArray(Object[] a)
был введен еще в Java 1.2.
Соответствующий родовой, который заменяет Object
с T
был представлен как 100% обратно совместимая опция:
public <T> T[] toArray(T[] a)
Изменение подписи на generic позволяет вызывающим сторонам избегать приведения: до Java 5 вызывающие абоненты должны были делать это:
String[] arr = (String[])stringList.toArray(new String[stringList.size()]);
Теперь они могут сделать один и тот же вызов без приведения:
String[] arr = stringList.toArray(new String[stringList.size()]);
РЕДАКТИРОВАТЬ:
Более "современная" подпись для toArray
Метод будет парой перегрузок:
public <T> T[] toArray(Class<T> elementType)
public <T> T[] toArray(Class<T> elementType, int count)
Это обеспечило бы более выразительную и в равной степени универсальную альтернативу текущей сигнатуре метода. Существует эффективная реализация этого тоже с Array.newInstance(Class<T>,int)
метод на месте. Однако изменение подписи не будет обратно совместимым.
Это безопасно для типов - это не вызывает ClassCastException
, Это вообще то, что означает типобезопасность.
ArrayStoreException
это отличается. Если вы включите ArrayStoreException
в "не типобезопасном" все массивы в Java не являются типобезопасными.
Код, который вы разместили, также производит ArrayStoreException
, Просто попробуй:
TestGenerics<Object> list = new TestGenerics<Object>();
list.add(1);
String[] n = new String[10];
list.toArray(n); // ArrayStoreException
На самом деле, просто невозможно разрешить пользователю передавать массив того типа, который он хочет получить, и в то же время не иметь ArrayStoreException
, Потому что любая сигнатура метода, которая принимает массив некоторого типа, также допускает массивы подтипов.
Так как избежать невозможно ArrayStoreException
почему бы не сделать его как можно более универсальным? Чтобы пользователь мог использовать массив некоторого несвязанного типа, если он каким-то образом знает, что все элементы будут экземплярами этого типа?
Причина в том, что этот метод в основном исторический.
Существует различие между универсальными классами и типами массивов: тогда как параметры типа универсального класса стираются во время выполнения, тип элементов массивов - нет. Таким образом, во время выполнения JVM не видит разницы между List<Integer>
а также List<String>
, но он видит разницу между Integer[]
а также String[]
! Причина этого различия заключается в том, что массивы всегда были там, начиная с Java 1.0 и выше, тогда как дженерики добавлялись (обратно совместимым образом) в Java 1.5.
API коллекций был добавлен в Java 1.2 до введения обобщений. В то время List
интерфейс уже содержал метод
Object[] toArray(Object[] a);
(см. эту копию 1.2 JavaDoc). Это был единственный способ создать массив с указанным пользователем типом времени выполнения: параметр a
служил токеном типа, то есть определял тип времени выполнения возвращаемого массива (обратите внимание, что если A
это подкласс B
, A[]
считается подтипом B[]
хотя List<A>
не является подтипом List<B>
).
Когда дженерики были введены в Java 1.5, многие существующие методы стали дженериками, и toArray
метод стал
<T> T[] toArray(T[] a);
который после стирания типа имеет ту же сигнатуру, что и оригинальный неуниверсальный метод.
Я думаю, что dasblinkenlight, вероятно, правильно, что это как-то связано с генерированием существующего метода, а полная совместимость - тонкая вещь, которую нужно достичь.
Точка зрения beny23 тоже очень хорошая - метод должен принимать супертипы E[]
, Можно попытаться
<T super E> T[] toArray(T[] a)
но Java не позволяет super
на переменную типа, из-за отсутствия вариантов использования:)
(редактировать: нет, это не хороший вариант использования для super
см. /questions/26502243/svyazyivayuschie-dzheneriki-s-klyuchevyim-slovom-super/26502250#26502250)
Этот код жестоко скопирован из исходного кода класса java.util.LinkedList:
public <T> T[] toArray(T[] a) {
if (a.length < size)
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size)
// actual code for copying
return a;
}
Это должно сделать трюк
Наконец, с Java 11 у нас есть хороший компромисс:
// Class Collection
default <T> T[] toArray(IntFunction<T[]> generator) {
return toArray(generator.apply(0));
}
Итак, если вы предоставляете конструктор массива, он создает и копирует данные из коллекции в примитивный массив:
Set.of("a", "b").toArray(String[]::new);
List.of(1L, 2L).toArray(Long[]::new);