Java дженерики в ArrayList.toArray()


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

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

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

String someLine = someData.get(0);

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

String[] arrayOfData = someData.toArray();

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

7 54

7 ответов:

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

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's 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 представляет. Другими словами создание массива невоспроизводимого типа (JLS §4.7)не допускается в Java.

еще одна важная цитата из Исключение Хранилища Массивов (JLS §10.5):

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

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

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

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

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

Общая информация удалены во время выполнения. 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's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());

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

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

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

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

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

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

я могу и буду использовать итератор вместо того, чтобы иногда создавать массив, но это всегда казалось мне странным. Почему метод 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 runtime не имеет конкретной информации типа, представленного E. Единственным обходным путем на данный момент является передача требуемого типа в качестве параметра метода и, следовательно, сигнатуры public <T> T[] toArray(T[] a) где клиенты вынуждены передавать требуемый тип.

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

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

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

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

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

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

теперь во время компиляции тип T стирается. Как должна быть часть new T[length] заменить? Общие сведения о типе недоступны.

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

самое первое, что вы должны понять, что ArrayList собственный-это просто массив Object

   transient Object[] elementData;

когда дело доходит до причин, почему T[] это ошибка, потому что вы не можете получить массив универсального типа без Class<T> и это потому, что тип java erase (есть еще объяснение и как создать один). А то 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().