По какой причине я не могу создать универсальные типы массивов в Java?

Почему Java не позволяет нам делать

private T[] elements = new T[initialCapacity];

Я мог понять, что.NET не позволил нам сделать это, так как в.NET у вас есть типы значений, которые во время выполнения могут иметь разные размеры, но в Java все виды T будут ссылками на объекты, поэтому имеют одинаковый размер (поправьте меня если я ошибаюсь).

Какова причина?

18 ответов

Решение

Это потому, что массивы Java (в отличие от обобщенных) содержат во время выполнения информацию о типе его компонента. Таким образом, вы должны знать тип компонента при создании массива. Так как ты не знаешь что T во время выполнения, вы не можете создать массив.

Цитата:

Массивы универсальных типов не допускаются, потому что они не являются звуковыми. Проблема возникает из-за взаимодействия массивов Java, которые не являются статически исправными, но динамически проверяются, с обобщениями, которые являются статически исправными и не проверяются динамически. Вот как вы могли бы использовать лазейку:

class Box<T> {
    final T x;
    Box(T x) {
        this.x = x;
    }
}

class Loophole {
    public static void main(String[] args) {
        Box<String>[] bsa = new Box<String>[3];
        Object[] oa = bsa;
        oa[0] = new Box<Integer>(3); // error not caught by array store check
        String s = bsa[0].x; // BOOM!
    }
}

Мы предложили решить эту проблему, используя статически безопасные массивы (aka Variance), которые были отклонены для Tiger.

- гаптер

(Я верю, что это Neal Gafter, но я не уверен)

Посмотрите это в контексте здесь: http://forums.sun.com/thread.jspa?threadID=457033&forumID=316

Не предоставив достойного решения, вы просто получите что-то худшее ИМХО.

Общая работа заключается в следующем.

T[] ts = new T[n];

заменяется на (при условии, что T расширяет Object, а не другой класс)

T[] ts = (T[]) new Object[n];

Я предпочитаю первый пример, однако больше академических типов, кажется, предпочитают второй, или просто предпочитают не думать об этом.

Большинство примеров того, почему вы не можете просто использовать Object [], в равной степени применимы к List или Collection (которые поддерживаются), поэтому я вижу их как очень плохие аргументы.

Примечание: это одна из причин, по которой библиотека Collections сама не компилируется без предупреждений. Если этот вариант использования не может быть поддержан без предупреждений, то что-то в корне сломано с моделью обобщенных типов IMHO.

Массивы ковариантны

Говорят, что массивы ковариантны, что в основном означает, что, учитывая правила подтипирования Java, массив типа T[] может содержать элементы типа T или любого подтипа T. Например,

Number[] numbers = newNumber[3];
numbers[0] = newInteger(10);
numbers[1] = newDouble(3.14);
numbers[2] = newByte(0);

Но не только это, правила подтипирования Java также утверждают, что массив S[] является подтипом массива T[], если S является подтипом T, поэтому что-то подобное также допустимо:

Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;

Поскольку в соответствии с правилами подтипирования в Java, массив Integer[] является подтипом массива Number[], поскольку Integer является подтипом Number.

Но это правило подтипов может привести к интересному вопросу: что произойдет, если мы попытаемся это сделать?

myNumber[0] = 3.14; //attempt of heap pollution

Эта последняя строка прекрасно скомпилируется, но если мы запустим этот код, мы получим ArrayStoreException, потому что мы пытаемся поместить double в целочисленный массив. Тот факт, что мы обращаемся к массиву через ссылку Number, здесь не имеет значения, важно то, что массив является массивом целых чисел.

Это означает, что мы можем обмануть компилятор, но мы не можем обмануть систему типов во время выполнения. И это так, потому что массивы - это то, что мы называем типом reifiable. Это означает, что во время выполнения Java знает, что этот массив был фактически создан как массив целых чисел, к которым просто случается обращение через ссылку типа Number[].

Итак, как мы видим, одна вещь - это фактический тип объекта, другая вещь - это тип ссылки, которую мы используем для доступа к ней, верно?

Проблема с Java Generics

Теперь проблема с универсальными типами в Java заключается в том, что информация о типе для параметров типа отбрасывается компилятором после завершения компиляции кода; поэтому эта информация типа не доступна во время выполнения. Этот процесс называется стиранием типа. Существуют веские причины для реализации таких обобщений в Java, но это длинная история, и она связана с бинарной совместимостью с уже существующим кодом.

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

Теперь рассмотрим следующий небезопасный код:

List<Integer> myInts = newArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution

Если компилятор Java не мешает нам сделать это, система типов во время выполнения также не может остановить нас, потому что во время выполнения нет никакого способа определить, что этот список должен быть только списком целых чисел. Среда выполнения Java позволила бы нам помещать все, что мы хотим, в этот список, когда он должен содержать только целые числа, потому что когда он был создан, он был объявлен как список целых чисел. Вот почему компилятор отклоняет строку № 4, потому что это небезопасно и, если разрешено, может нарушить предположения системы типов.

Поэтому разработчики Java позаботились о том, чтобы мы не могли обмануть компилятор. Если мы не можем обмануть компилятор (как мы можем сделать с массивами), то мы не можем обмануть и систему типов во время выполнения.

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

Я пропустил некоторые части этих ответов, вы можете прочитать полную статью здесь: https://dzone.com/articles/covariance-and-contravariance

Причина, по которой это невозможно, состоит в том, что Java реализует свои Generics исключительно на уровне компилятора, и для каждого класса генерируется только один файл класса. Это называется стиранием типа.

Во время выполнения скомпилированный класс должен обрабатывать все его использования с одним и тем же байт-кодом. Так, new T[capacity] не имел бы абсолютно никакого представления о том, какой тип должен быть создан.

Ответ уже был дан, но если у вас уже есть экземпляр T, вы можете сделать это:

T t; //Assuming you already have this object instantiated or given by parameter.
int length;
T[] ts = (T[]) Array.newInstance(t.getClass(), length);

Надеюсь, я смогу помочь, Ferdi265

Основная причина заключается в том, что массивы в Java ковариантны.

Здесь есть хороший обзор.

Мне нравится ответ, косвенно предоставленный Gafter. Однако я предлагаю это неправильно. Я немного изменил код Gafter. Он компилируется и работает какое-то время, а затем бомбит, где Гафтер предсказал

class Box<T> {

    final T x;

    Box(T x) {
        this.x = x;
    }
}

class Loophole {

    public static <T> T[] array(final T... values) {
        return (values);
    }

    public static void main(String[] args) {

        Box<String> a = new Box("Hello");
        Box<String> b = new Box("World");
        Box<String> c = new Box("!!!!!!!!!!!");
        Box<String>[] bsa = array(a, b, c);
        System.out.println("I created an array of generics.");

        Object[] oa = bsa;
        oa[0] = new Box<Integer>(3);
        System.out.println("error not caught by array store check");

        try {
            String s = bsa[0].x;
        } catch (ClassCastException cause) {
            System.out.println("BOOM!");
            cause.printStackTrace();
        }
    }
}

Выход

I created an array of generics.
error not caught by array store check
BOOM!
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at Loophole.main(Box.java:26)

Поэтому мне кажется, что вы можете создавать универсальные типы массивов в Java. Я неправильно понял вопрос?

В моем случае я просто хотел массив стеков, что-то вроде этого:

Stack<SomeType>[] stacks = new Stack<SomeType>[2];

Поскольку это было невозможно, я использовал следующее в качестве обходного пути:

  1. Создал не универсальный класс-оболочку для Stack (скажем, MyStack)
  2. MyStack [] stacks = new MyStack [2] работал отлично

Ужасно, но Ява счастлива.

Примечание: как упомянуто BrainSlugs83 в комментарии к вопросу, вполне возможно иметь массивы обобщений в.NET

Из учебника Oracle:

Вы не можете создавать массивы параметризованных типов. Например, следующий код не компилируется:

List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error

Следующий код иллюстрирует, что происходит, когда в массив вставляются разные типы:

Object[] strings = new String[2];
strings[0] = "hi";   // OK
strings[1] = 100;    // An ArrayStoreException is thrown.

Если вы попробуете то же самое с общим списком, возникнет проблема:

Object[] stringLists = new List<String>[];  // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>();   // OK
stringLists[1] = new ArrayList<Integer>();  // An ArrayStoreException should be thrown,
                                            // but the runtime can't detect it.

Если бы разрешались массивы параметризованных списков, предыдущий код не смог бы выдать желаемое исключение ArrayStoreException.

Для меня это звучит очень слабо. Я думаю, что любой, обладающий достаточным пониманием дженериков, будет в порядке и даже ожидает, что ArrayStoredException не будет выброшено в таком случае.

Я знаю, что немного опоздал на вечеринку здесь, но я подумал, что смогу помочь любым будущим гуглерам, так как ни один из этих ответов не решил мою проблему. Ответ Ferdi265 очень помог, хотя.

Я пытаюсь создать свой собственный связанный список, поэтому следующий код работает для меня:

package myList;
import java.lang.reflect.Array;

public class MyList<TYPE>  {

    private Node<TYPE> header = null;

    public void clear() {   header = null;  }

    public void add(TYPE t) {   header = new Node<TYPE>(t,header);    }

    public TYPE get(int position) {  return getNode(position).getObject();  }

    @SuppressWarnings("unchecked")
    public TYPE[] toArray() {       
        TYPE[] result = (TYPE[])Array.newInstance(header.getObject().getClass(),size());        
        for(int i=0 ; i<size() ; i++)   result[i] = get(i); 
        return result;
    }


    public int size(){
         int i = 0;   
         Node<TYPE> current = header;
         while(current != null) {   
           current = current.getNext();
           i++;
        }
        return i;
    }  

В методе toArray() заключается способ создания массива универсального типа для меня:

TYPE[] result = (TYPE[])Array.newInstance(header.getObject().getClass(),size());    

Если класс использует как параметризованный тип, он может объявить массив типа T[], но не может напрямую создать экземпляр такого массива. Вместо этого общий подход состоит в том, чтобы создать экземпляр массива типа Object[], а затем выполнить сужающее приведение к типу T[], как показано ниже:

  public class Portfolio<T> {
  T[] data;
 public Portfolio(int capacity) {
   data = new T[capacity];                 // illegal; compiler error
   data = (T[]) new Object[capacity];      // legal, but compiler warning
 }
 public T get(int index) { return data[index]; }
 public void set(int index, T element) { data[index] = element; }
}

Это потому, что дженерики были добавлены в java после того, как они его сделали, поэтому это немного неуклюже, потому что первоначальные создатели java подумали, что при создании массива тип будет указан при его создании. Так что это не работает с генериками, поэтому вы должны сделать E[] array=(E[]) new Object[15]; Это компилируется, но выдает предупреждение.

Если мы не можем создать экземпляры универсальных массивов, почему язык имеет универсальные типы массивов? Какой смысл иметь тип без объектов?

Единственная причина, по которой я могу думать, это varargs - foo(T...), В противном случае они могут иметь полностью очищенные универсальные типы массивов. (Ну, им действительно не нужно было использовать массив для varargs, поскольку varargs не существовало до 1.5. Это, вероятно, еще одна ошибка.)

Так что это ложь, вы можете создавать экземпляры универсальных массивов через varargs!

Конечно, проблемы с универсальными массивами все еще актуальны, например,

static <T> T[] foo(T... args){
    return args;
}
static <T> T[] foo2(T a1, T a2){
    return foo(a1, a2);
}

public static void main(String[] args){
    String[] x2 = foo2("a", "b"); // heap pollution!
}

Мы можем использовать этот пример, чтобы фактически продемонстрировать опасность универсального массива.

С другой стороны, мы использовали общие varargs в течение десятилетия, и небо еще не падает. Таким образом, мы можем утверждать, что проблемы преувеличиваются; Это не важно. Если явное создание универсального массива разрешено, у нас будут ошибки здесь и там; но мы привыкли к проблемам стирания, и мы можем жить с этим.

И мы можем указать на foo2 опровергнуть утверждение о том, что спецификация удерживает нас от проблем, от которых, по их утверждению, нас не пускают. Если бы у Sun было больше времени и ресурсов для 1,5, я думаю, они могли бы достичь более удовлетворительного решения.

Безусловно, должен быть хороший способ обойти это (возможно, с помощью отражения), потому что мне кажется, что это именно то, что ArrayList.toArray(T[] a) делает. Я цитирую:

public <T> T[] toArray(T[] a)

Возвращает массив, содержащий все элементы в этом списке в правильном порядке; тип времени выполнения возвращаемого массива является типом указанного массива. Если список помещается в указанный массив, он возвращается в него. В противном случае новый массив выделяется с типом времени выполнения указанного массива и размером этого списка.

Так что одним из способов было бы использовать эту функцию, т.е. создать ArrayList объектов, которые вы хотите в массиве, затем используйте toArray(T[] a) создать фактический массив. Это не было бы быстро, но вы не упомянули свои требования.

Так кто-нибудь знает, как toArray(T[] a) реализовано?

T vals[]; // ХОРОШО

Но вы не можете создать массив из T // vals = new T[10]; // не могу создать массив из T

Причина, по которой вы не можете создать массив T, заключается в том, что компилятор не может узнать, какой тип массива фактически создать.

Как уже упоминалось, вы, конечно, можете создавать с помощью некоторых хитростей.

Но это не рекомендуется.

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

Object[] stringArray = { "hi", "me" };
stringArray[1] = 1;
String aString = (String) stringArray[1]; // boom! the TypeCastException

Более прямой пример можно найти в Effective Java: пункт 25.


ковариация: массив типа S[] является подтипом T[], если S является подтипом T

Попробуй это:

List<?>[] arrayOfLists = new List<?>[4];
Другие вопросы по тегам