Чому я не можу створювати загальні типи масивів на Java?


273

У чому причина того, що Java не дозволяє нам це робити

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

Я міг зрозуміти .NET не дозволив нам це зробити, так як в .NET у вас є типи значень, які під час виконання можуть мати різні розміри, але в Java всі види T будуть посиланнями на об'єкти, таким чином, мають однаковий розмір ( виправте мене, якщо я помиляюся).

В чому причина?


29
Про що ти говориш? Ви можете зробити це абсолютно в .NET. - Я тут намагаюся з'ясувати, чому я не можу це зробити на Java.
BrainSlugs83

@ BrainSlugs83 - додайте посилання на приклад коду чи підручник, який це показує.
MasterJoe2


1
@ MasterJoe2 вищевказаний код у питанні ОП - це те, про що я маю на увазі. Це добре працює в C #, але не в Java. - Питання стверджує, що воно не працює ні в одному, що невірно. - Не впевнений, що є корисним для подальшого обговорення.
BrainSlugs83

Відповіді:


204

Це тому, що масиви Java (на відміну від генеричних) містять під час виконання інформацію про тип її компонента. Отже, ви повинні знати тип компонента під час створення масиву. Оскільки ви не знаєте, що Tзнаходиться під час виконання, ви не можете створити масив.


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[](саме так воно є в останньому джерелі Java Oracle); або 2) вихідний код містить змінну типу T[], що є брехнею, але не викликає проблем із-за Tстирання в межах класу.
newacct

137

Цитата:

Масиви загальних типів заборонені, оскільки вони не є здоровими. Проблема пов'язана з взаємодією масивів 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!
    }
}

Ми запропонували вирішити цю проблему за допомогою статично безпечних масивів (ака 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], що може бути часом достатньо, хоча це не допоможе у вашому прикладі.
Bartosz Klimek

1
@BartKiers Я не розумію ... це все ще не компілюється (java-8): Box<String>[] bsa = new Box<String>[3];чи щось змінилося в java-8 і вище я припускаю?
Євген

1
@Eugene, масиви конкретних загальних типів просто не дозволені, оскільки вони можуть призвести до втрати безпеки типу, як показано у зразку. Це заборонено в жодній версії Java. Відповідь починається так: "Масиви загальних типів заборонені, оскільки вони не є здоровими"
гранат

47

Якщо не надати гідного рішення, ви просто закінчите щось гірше IMHO.

Загальна робота навколо полягає в наступному.

T[] ts = new T[n];

замінюється на (якщо Т поширює Об'єкт, а не інший клас)

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

Я віддаю перевагу першому прикладу, проте більш академічні типи, здається, віддають перевагу другому, або просто не хочу думати про це.

Більшість прикладів того, чому ви не можете просто використовувати Object [], однаково стосуються списку або колекції (які підтримуються), тому я вважаю їх дуже поганими аргументами.

Примітка. Це одна з причин, що сама бібліотека колекцій не збирається без попереджень. Якщо цей випадок використання не може бути підтриманий без попереджень, щось із принциповим чином порушено із загальною моделлю IMHO.


6
Ви повинні бути обережними з другим. Якщо ви повернете створений таким чином масив тому, хто очікує, скажімо, String[](або якщо ви зберігаєте його в полі, яке є загальнодоступним типу T[], і хтось його отримує), вони отримають ClassCastException.
newacct

4
Я проголосував за цю відповідь, тому що ваш бажаний приклад заборонений у Java, а ваш другий приклад може кинути ClassCastException
Хосе Роберто

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);

Але не тільки це, правила S[]підтипу Java також стверджують, що масив є підтипом масиву, T[]якщо Sє підтипом T, отже, щось подібне також є дійсним:

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

Тому що згідно з правилами Integer[]підтипу на Java, масив є підтипом масиву, Number[]оскільки Integer є підтипом Number.

Але це правило підтипу може призвести до цікавого питання: що буде, якщо ми спробуємо це зробити?

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

Цей останній рядок складеться просто чудово, але якщо ми запустимо цей код, ми отримаємо, ArrayStoreExceptionтому що ми намагаємось поставити двійник у цілий масив. Те, що ми отримуємо доступ до масиву через посилання Number, тут не має значення, що важливо, це те, що масив є масивом цілих чисел.

Це означає, що ми можемо обдурити компілятор, але не можемо обдурити систему типу виконання. І це так, тому що масиви - це те, що ми називаємо повторюваним типом. Це означає, що під час виконання 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]не мав би абсолютно ніякого уявлення про те, який тип потрібно приміркувати.


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);

Сподіваюся, я можу допомогти, Ферді265


1
Це приємне рішення. Але це отримає неперевірені попередження (передано з Object to 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в цій відповіді немає масиву.
xmen

6

Основна причина пов'язана з тим, що масиви на Java є коваріантними.

Там є огляд добре тут .


Я не бачу, як ви могли б підтримувати "новий T [5]" навіть з інваріантними масивами.
Димитріс Андреу

2
@DimitrisAndreou Ну, вся справа - це скоріше комедія помилок у дизайні Java. Все почалося з коваріації масиву. Потім, коли у вас є масив ковариации, ви можете кинути String[]в Objectі зберігати Integerв ньому. Тоді їм довелося додати перевірку типу виконання для магазинів масивів ( ArrayStoreException), оскільки проблему не вдалося вирішити під час компіляції. (Інакше Integerнасправді можна застрягнути в a String[], і ви отримаєте помилку, коли
спробуєте

2
@DimitrisAndreou… Потім, після того, як ви поставили перевірку часу замість перевірки часу компіляції з далеким звуком, ви стикаєтесь зі стиранням типу (також прикрий недолік дизайну - включений лише для зворотної сумісності). Стирання типу означає, що ви не можете робити перевірки типу виконання для загальних типів. Отже, щоб уникнути проблеми типу зберігання масиву, ви просто не можете мати загальні масиви. Якби вони просто зробили масиви інваріантними в першу чергу, ми могли б просто зробити перевірку типу компіляції в часі, не запускаючи стирання.
Радон Росборо

… Я щойно відкрив п’ятихвилинний період редагування коментарів. Objectповинен був бути Object[]в моєму першому коментарі.
Радон Росборо

3

Мені подобається відповідь, опосередковано дана Гафтером . Однак я вважаю, що це неправильно. Я трохи змінив код Гафтера. Він збирається, і він працює деякий час, потім він бомбить там, де Гафтер передбачив, що це буде

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
Так, я отримую попередження про безпеку типу. Так, я бачу, що мій приклад не відповідає на запитання.
emory

Насправді ви отримуєте кілька попереджень через неохайну ініціалізацію a, b, c. Також це добре відомо і впливає на основну бібліотеку, наприклад, <T> java.util.Arrays.asList (T ...). Якщо ви передасте будь-який не повторюваний тип для T, ви отримуєте попередження (оскільки створений масив має менш точний тип, ніж прикидається код), і це супер некрасиво. Було б краще, якби автор цього методу отримав попередження, а не випромінював його на сайті використання, враховуючи, що сам метод безпечний, він не піддає користувачеві масив.
Димитріс Андреу

1
Тут ви не створили загальний масив. Компілятор створив для вас (не загальний) масив.
newacct

2

Я знаю, що тут я трохи запізнююся на вечірку, але я подумав, що можу допомогти будь-яким майбутнім гуглерам, оскільки жоден із цих відповідей не вирішив мого питання. Але відповідь Ферді265 надзвичайно допомогла.

Я намагаюся створити свій власний список пов’язаних, тому наступний код - це те, що працювало для мене:

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. Створив нестандартний клас обгортки навколо Stack (скажімо, MyStack)
  2. MyStack [] стеки = нові 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 не буде кинутий у такому випадку.


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 []) новий Object [15]; Це компілюється, але дає попередження.


0

Якщо ми не можемо створити загальні масиви, чому мова має типові масиви? Який сенс мати тип без об’єктів?

Єдина причина, про яку я можу подумати, - це вараги 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!
}

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

З іншого боку, ми використовуємо загальні вараги вже десятиліття, і небо ще не падає. Тож можна стверджувати, що проблеми перебільшуються; це не велика справа. Якщо дозволено створення явного загального масиву, ми будемо мати помилки тут і там; але ми звикли до проблем стирання, і ми можемо з цим жити.

І ми можемо вказати на foo2спростування твердження про те, що специфікація утримує нас від проблем, які вони стверджують, що нас утримують. Якби у Sun було більше часу та ресурсів на 1,5 , я вважаю, що вони могли б досягти більш задоволення.


0

Як вже згадували інші, ви, звичайно, можете створити за допомогою деяких хитрощів .

Але це не рекомендується.

Оскільки стирання типу і, що ще важливіше, covarianceмасив масиву, який просто дозволяє масиві підтипів, може бути призначений масиву супертипів, що змушує вас використовувати явний тип передачі при спробі повернути значення, викликаючи час виконання, ClassCastExceptionщо є однією з головних цілей що дженерики намагаються усунути: Сильніші перевірки типу під час компіляції .

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

Більш прямий приклад можна знайти в Ефективній Java: Пункт 25 .


коваріація : масив типу S [] є підтипом T [], якщо S є підтипом T


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; }
}

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.