Подтвердить что ты не робот

Java-генерики в ArrayList.toArray()

Скажите, что у вас есть arraylist, который определяется следующим образом:

ArrayList<String> someData = new ArrayList<>();

Позже в вашем коде из-за дженериков вы можете сказать следующее:

String someLine = someData.get(0);

И компилятор точно знает, что он получит строку. Да, дженерики! Однако это не удастся:

String[] arrayOfData = someData.toArray();

toArray() всегда будет возвращать массив объектов, а не общий, который был определен. Почему метод get(x) знает, что он возвращает, но toArray() по умолчанию для объектов?

4b9b3361

Ответ 1

Если вы посмотрите на реализацию toArray(T[] a) класса ArrayList<E>, это выглядит так:

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

Проблема с этим методом в том, что вам нужно передать массив того же универсального типа. Теперь рассмотрим, если этот метод не принимает аргументов, реализация будет выглядеть примерно так:

public <T> T[] toArray() {
    T[] t = new T[size]; // compilation error
    return Arrays.copyOf(elementData, size, t.getClass());
}

Но проблема здесь в том, что вы не можете создавать универсальные массивы в Java, потому что компилятор не знает точно, что представляет T Другими словами, создание массива типа non-reifiable (JLS §4.7) не разрешено в Java.

Еще одна важная цитата из Array Store Exception (JLS §10.5):

Если тип компонента массива не был пригоден для повторного использования (§4.7), виртуальная машина Java не могла выполнить проверку хранилища, описанную в предыдущем абзаце. Вот почему выражение создания массива с типом элемента non-reifiable запрещено (§15.10.1).

Вот почему Java предоставила перегруженную версию toArray(T[] a).

Я переопределю метод toArray(), чтобы сообщить ему, что он вернет массив E.

Поэтому вместо переопределения toArray() вы должны использовать toArray(T[] a).

Невозможно создать экземпляры параметров типа из Java Doc.

Ответ 2

Общая информация удалена во время выполнения. JVM не знает, является ли ваш список List<String> или List<Integer> (во время выполнения T в List<T> разрешен как Object), поэтому единственным возможным типом массива является Object[].

Вы можете использовать toArray(T[] array), хотя - в этом случае JVM может использовать класс данного массива, вы можете увидеть его в реализации ArrayList:

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());

Ответ 3

Если вы посмотрите на Javadoc для интерфейса List, вы увидите вторую форму toArray: <T> T[] toArray(T[] a).

Фактически, Javadoc даже дает пример того, как делать именно то, что вы хотите сделать:

String[] y = x.toArray(new String[0]);

Ответ 4

Я могу использовать итератор вместо того, чтобы иногда создавать массив, но это всегда казалось мне странным. Почему метод get (x) знает, что он возвращает, но toArray() по умолчанию относится к объектам? Его наполовину в проектирование, они решили, что здесь не нужно было?

Поскольку цель вопроса состоит не только в том, чтобы обойтись с помощью toArray() с помощью дженериков, а также по пониманию дизайна методов в классе ArrayList, я хотел бы добавить:

ArrayList - это общий класс, поскольку он объявлен как

public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable

что позволяет использовать в классе общие методы, такие как public E get(int index).

Но если метод, такой как toArray(), не возвращает E, а скорее E[], тогда все становится немного сложнее. Было бы невозможно предложить такую ​​подпись, как public <E> E[] toArray(), потому что невозможно создать общие массивы.

Создание массивов происходит во время выполнения и из-за Тип стирания, время выполнения Java не имеет конкретной информации о типе, представленном E. Единственным обходным решением на данный момент является передача требуемого типа в качестве параметра для метода и, следовательно, подпись public <T> T[] toArray(T[] a), где клиенты вынуждены передавать требуемый тип.

Но, с другой стороны, он работает для public E get(int index), потому что, если вы посмотрите на реализацию метода, вы обнаружите, что даже при том, что метод использует один и тот же массив объекта для возврата элемента по указанному индексу, он отбрасывается до E

E elementData(int index) {
    return (E) elementData[index];
}

Это компилятор Java, который во время компиляции заменяет E на Object

Ответ 5

Уместно отметить, что массивы в Java знают свой тип компонента во время выполнения. String[] и Integer[] - это разные классы во время выполнения, и вы можете запрашивать массивы для их типа компонента во время выполнения. Поэтому во время выполнения требуется тип компонента (либо путем жесткого кодирования повторяемого типа компонента во время компиляции с помощью new String[...], либо с помощью Array.newInstance() и передачи объекта класса) для создания массива.

С другой стороны, аргументы типа в generics не существуют во время выполнения. Во время выполнения между ArrayList<String> и a ArrayList<Integer> нет никакой разницы. Это всего лишь ArrayList.

Это основная причина, по которой вы не можете просто взять List<String> и получить String[] без какой-либо передачи типа компонента отдельно - вам нужно будет получить информацию о типе компонента из того, что не имеет информация о компоненте. Ясно, что это невозможно.

Ответ 6

Массив имеет другой тип, чем тип массива. Это класс StringArray вместо класса String.

Предполагая, возможно, что общий метод toArray() будет выглядеть как

private <T> T[] toArray() {
    T[] result = new T[length];
    //populate
    return result;
}

Теперь во время компиляции тип T стирается. Как заменить часть new T[length]? Информация о типовом типе недоступна.

Если вы посмотрите на исходный код (например) ArrayList, вы увидите то же самое. Метод toArray(T[] a) либо заполняет данный массив (если размер соответствует), либо создает новый новый массив, используя тип параметра, который является типом массива Generic Type T.

Ответ 7

Самое первое, что вам нужно понять, это то, что ArrayList own - это всего лишь массив Object

   transient Object[] elementData;

Когда дело доходит до того, что T[] терпит неудачу, это потому, что вы не можете получить массив родового типа без Class<T>, и это связано с тем, что стирание типа java (есть больше объяснений и как создать один). И array[] в куче знает свой тип динамически, и вы не можете отбрасывать int[] на String[]. По той же причине вы не можете отбрасывать Object[] в T[].

   int[] ints = new int[3];
   String[] strings = (String[]) ints;//java: incompatible types: int[] cannot be converted to java.lang.String[]

   public <T> T[] a() {
      Object[] objects = new Object[3];
      return (T[])objects;
   }
   //ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
   Integer[] a = new LearnArray().<Integer>a();

Но то, что вы помещаете в array, - это просто объект, тип которого E (который проверяется компилятором), поэтому вы можете просто направить его на E, который является безопасным и правильным.

  return (E) elementData[index];

Короче говоря, вы не можете получить то, чего нет в результате броска. У вас есть только Object[], поэтому toArray() может просто вернуть Object[] (в противном случае вы должны дать ему Class<T>, чтобы создать новый массив с этим типом). Вы помещаете E в ArrayList<E>, вы можете получить E с помощью get().

Ответ 8

Можно создать "универсальный" массив заданного (известного) типа. Обычно я использую что-то подобное в моем коде.

public static <T> T[] toArray(Class<T> type, ArrayList<T> arrList) {
    if ((arrList == null) || (arrList.size() == 0)) return null;
    Object arr = Array.newInstance(type, arrList.size());
    for (int i=0; i < arrList.size(); i++) Array.set(arr, i, arrList.get(i));
    return (T[])arr;
}