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

273

Какова причина, почему Java не позволяет нам делать

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

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

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

пожрал Элизиум
источник
29
О чем ты говоришь? Вы можете сделать это в .NET. - Я здесь пытаюсь понять, почему я не могу сделать это на Java.
BrainSlugs83
@ BrainSlugs83 - пожалуйста, добавьте ссылку на пример кода или учебник, который показывает это.
MasterJoe2
Также смотрите - stackoverflow.com/questions/21577493/…
MasterJoe2
1
@ MasterJoe2 приведенный выше код в вопросе ОП - это то, что я имею в виду. Он отлично работает в C #, но не в Java. - Вопрос гласит, что это не работает ни в одном, что неверно. - Не уверен, что стоит обсуждать это дальше.
BrainSlugs83

Ответы:

204

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

newacct
источник
29
Но как насчет стирания? Почему это не относится?
Qix - МОНИКА БЫЛА НЕЗАБЫВАЕМАЯ
14
Как это ArrayList <SomeType>сделать тогда?
Thumbz
10
@ Thumbz: Вы имеете в виду new ArrayList<SomeType>()? Универсальные типы не содержат параметр типа во время выполнения. Параметр типа не используется при создании. Нет разницы в коде, созданном new ArrayList<SomeType>()или new ArrayList<String>()или new ArrayList()вообще.
newacct
8
Я спрашивал больше о том, как ArrayList<T>работает с ней » private T[] myArray. Где-то в коде он должен иметь массив универсального типа T, так как?
Thumbz
21
@Thumbz: у него нет массива типа времени выполнения T[]. У него есть массив типа времени выполнения Object[], и либо 1) исходный код содержит переменную Object[](это так в последней версии Oracle Java source); или 2) исходный код содержит переменную типа T[], которая является ложью, но не вызывает проблем из-за Tудаления внутри области видимости класса.
newacct
137

Quote:

Массивы общих типов недопустимы, потому что они не являются звуковыми. Проблема возникает из-за взаимодействия массивов 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.

- гаптер

(Я считаю, что это Нил Гафтер , но я не уверен)

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

Барт Киерс
источник
3
Обратите внимание, что я сделал это CW, так как ответ не мой.
Барт Киерс
10
Это объясняет, почему это может быть небезопасно. Но проблемы безопасности типов могут быть предупреждены компилятором. Дело в том, что это даже невозможно сделать по почти той же причине, по которой вы не можете этого сделать new T(). Каждый массив в Java по своему дизайну хранит тип компонента (т.е. T.class) внутри него; поэтому вам нужен класс T во время выполнения для создания такого массива.
newacct
2
Вы все еще можете использовать new Box<?>[n], что иногда может быть достаточно, хотя это не поможет в вашем примере.
Бартош Климек
1
@BartKiers Я не понимаю ... это все еще не компилируется (java-8): Box<String>[] bsa = new Box<String>[3];что-то изменилось в java-8 и выше, я полагаю?
Евгений
1
@ Евгений, массивы определенных универсальных типов просто не допускаются, потому что они могут привести к потере безопасности типов, как показано в примере. Это не разрешено ни в одной версии Java. Ответ начинается с того, что «массивы универсальных типов недопустимы, потому что они не
гранат
47

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

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

T[] ts = new T[n];

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

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

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

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

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

Питер Лори
источник
6
Вы должны быть осторожны со вторым. Если вы вернете массив, созданный таким образом, кому-то, кто ожидает, скажем, String[](или если вы сохраните его в поле, которое общедоступного типа T[], и кто-то получит его), он получит исключение ClassCastException.
newacct
4
Я отклонил этот ответ, потому что ваш предпочтительный пример не разрешен в Java, а ваш второй пример может вызвать исключение ClassCastException
José Roberto Araújo Júnior
5
@ JoséRobertoAraújoJúnior Совершенно очевидно, что первый пример необходимо заменить вторым примером. Было бы более полезно объяснить, почему второй пример может вызвать исключение ClassCastException, так как это не будет очевидно для всех.
Питер Лоури
3
@PeterLawrey Я создал вопрос с самоотвечением, показывающий, почему T[] ts = (T[]) new Object[n];это плохая идея: stackoverflow.com/questions/21577493/…
Хосе Роберто Араужо Жуниор
1
@MarkoTopolnik Мне следует дать медаль за ответы на все ваши комментарии, чтобы объяснить то же самое, что я уже сказал, единственное, что изменилось по сравнению с моей первоначальной причиной, это то, что я сказал, что он сказал, T[] ts = new T[n];был верным примером. Я сохраню голосование, потому что его ответ может вызвать проблемы и замешательство у других разработчиков, а также не по теме. Также я перестану комментировать по этому поводу.
Хосе Роберто Араужо Хуниор
38

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

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

Number[] numbers = new Number[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 является подтипом числа.

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

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

Хумоюн Ахмад
источник
32

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

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

Durandal
источник
17

Ответ уже был дан, но если у вас уже есть экземпляр 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

Ferdi265
источник
1
Это хорошее решение. Но это будет получать непроверенные предупреждения (приведение от объекта к T []). Другое «медленнее» , но решение «предупреждение свободного» будет: T[] ts = t.clone(); for (int i=0; i<ts.length; i++) ts[i] = null;.
Midnite
1
Кроме того, если то, что мы сохранили T[] t, то будет (T[]) Array.newInstance(t.getClass().getComponentType(), length);. я потратил несколько раз, чтобы выяснить getComponentType(). Надеюсь, что это помогает другим.
Midnite
1
@midnite t.clone()не вернется T[]. Потому что tэто не Array в этом ответе.
xmen
6

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

Там есть обзор хорошо здесь .

GaryF
источник
Я не понимаю, как вы могли бы поддерживать «новый T [5]» даже с инвариантными массивами.
Димитрис Андреу
2
@DimitrisAndreou Ну, все это скорее комедия ошибок в дизайне Java. Все началось с ковариации массива. Затем, когда у вас есть ковариация массива, вы можете привести String[]к нему Objectи сохранить Integerв нем. Затем им пришлось добавить проверку типа во время выполнения для массива store ( ArrayStoreException), потому что проблема не могла быть обнаружена во время компиляции. (В противном случае на Integerсамом деле может застрять в a String[], и вы получите сообщение об ошибке, когда попытаетесь получить его, что будет ужасно.) ...
Радон Росборо
2
@DimitrisAndreou… Затем, после того, как вы поместили проверку времени выполнения вместо проверки во время компиляции дальнего оповещения, вы столкнетесь со стиранием типов (также неудачный недостаток проекта - включен только для обратной совместимости). Стирание типа означает, что вы не можете выполнять проверки типов во время выполнения для универсальных типов. Поэтому, чтобы избежать проблемы с типом хранения массива, вы просто не можете иметь универсальные массивы. Если бы они просто сделали массивы инвариантными в первую очередь, мы могли бы просто выполнять проверки типов во время компиляции, не сталкиваясь со стиранием.
Радон Росборо
... Я только что обнаружил пятиминутный период редактирования комментариев. Objectдолжен был быть Object[]в моем первом комментарии.
Радон Росборо
3

Мне нравится ответ, косвенно предоставленный 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. Я неправильно понял вопрос?

Эмери
источник
Ваш пример отличается от того, что я просил. То, что вы описали, является опасностью ковариации массива. Проверьте это (для .NET: blogs.msdn.com/b/ericlippert/archive/2007/10/17/… )
пожрал Elysium
Надеюсь, вы получите предупреждение о безопасности типов от компилятора, да?
Мэтт МакГенри
1
Да, я получаю предупреждение о безопасности типов. Да, я вижу, что мой пример не отвечает на вопрос.
Эмори
На самом деле вы получаете несколько предупреждений из-за небрежной инициализации a, b, c. Кроме того, это хорошо известно и влияет на базовую библиотеку, например, <T> java.util.Arrays.asList (T ...). Если вы передадите какой-либо тип non-reifiable для T, вы получите предупреждение (потому что созданный массив имеет менее точный тип, чем претендует код), и это ужасно. Было бы лучше, если бы автор этого метода получил предупреждение, а не отправлял его на сайте использования, учитывая, что сам метод безопасен, он не предоставляет массив пользователю.
Димитрис Андреу
1
Вы не создали здесь общий массив. Компилятор создал (не универсальный) массив для вас.
newacct
2

Я знаю, что немного опоздал на вечеринку, но решил, что смогу помочь любым будущим гуглерам, так как ни один из этих ответов не решил мою проблему. Ответ 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());    
Дерек Зимба
источник
2

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

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

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

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

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

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

Давид Айрапетян
источник
2

Из учебника 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 не будет выброшено в таком случае.

Stick Hero
источник
0

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

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

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

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

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

Адам
источник
3
List.toArray (T []) работает, потому что вы по существу присваиваете ему тип компонента T во время выполнения (вы даете ему экземпляр нужного типа массива, из которого он может получить класс массива, а затем класс компонента T ). С фактическим типом компонента во время выполнения вы всегда можете создать массив этого типа времени выполнения, используя Array.newInstance(). Вы найдете упомянутое во многих вопросах, которые задают, как создать массив с типом, неизвестным во время компиляции. Но ОП специально спрашивал, почему вы не можете использовать new T[]синтаксис, а это другой вопрос
newacct
0

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

Alvin
источник
0

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

Единственная причина, по которой я могу думать, это 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 , я думаю, они могли бы достичь более удовлетворительного решения.

Zhongyu
источник
0

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

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

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

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

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


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

Hearen
источник
0

Если класс использует в качестве параметризованного типа, он может объявить массив типа 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; }
}
DeV
источник
-2

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

List<?>[] arrayOfLists = new List<?>[4];
Хорхе Вашингтон
источник