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

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

Хорошо, я прочитал много ответов на этот вопрос, но у меня более конкретный. В качестве примера возьмем следующий фрагмент кода.

public class GenericArray<E>{
    E[] s= new E[5];
}

После стирания стилей он становится

public class GenericArray{
    Object[] s= new Object[5];
}

Этот фрагмент кода, похоже, работает хорошо. Почему это вызывает ошибку времени компиляции?

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

public class GenericArray<E>{
    E[] s= (E[])new Object[5];
}

Я прочитал несколько комментариев, говоря, что часть кода выше небезопасна, но почему она небезопасна? Может ли кто-нибудь предоставить мне конкретный пример, где приведенная выше часть кода вызывает ошибку?

Кроме того, следующий код также неверен. Но почему? Кажется, что он хорошо работает и после стирания.

public class GenericArray<E>{
        E s= new E();
    }
4b9b3361

Ответ 1

Этот фрагмент кода, похоже, работает хорошо. Почему это вызывает ошибку времени компиляции?

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

Помните, что из-за стирания типа тип E не известен во время выполнения. Выражение new E[10] могло бы в лучшем случае создать массив стираемого типа, в данном случае Object, отображая исходное утверждение:

E[] s= new E[5];

Эквивалент:

E[] s= new Object[5];    

Это, конечно, не является законным. Например:

String[] s = new Object[10];

... не компилируется, по той же причине.

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

ArrayList<String> l = new ArrayList<Object>();

Стирание выше будет ArrayList l = new ArrayList();, что является законным, а оригинал явно не является.

Приступая к этому с более философского угла, тип erasure не должен изменять семантику кода, но в этом случае он будет делать это - созданный массив будет массивом Object, а не массивом E (любой E может быть). Тогда сохранение в нем ссылки на объект E было бы возможно, тогда как если бы массив был действительно E[], он должен вместо этого генерировать ArrayStoreException.

почему он небезопасен?

(имея в виду, что теперь мы говорим о случае, когда E[] s= new E[5]; был заменен на E[] s = (E[]) new Object[5];)

Это небезопасно (что в этом случае для типа небезопасно), потому что во время выполнения он создает ситуацию, в которой переменная (s) содержит ссылку на экземпляр объекта, который не является подтипом объявленный тип переменной (Object[] не является подтипом E[], если только E == Object).

Может ли кто-нибудь предоставить мне конкретный пример, где приведенная выше часть кода вызывает ошибку?

Существенная проблема заключается в том, что объекты без E можно помещать в массив, который вы создаете, выполняя приведение (как в (E[]) new Object[5]). Например, скажем, существует метод foo, который принимает параметр Object[], определяемый как:

void foo(Object [] oa) {
    oa[0] = new Object();
}

Затем возьмите следующий код:

String [] sa = new String[5];
foo(sa);
String s = sa[0]; // If this line was reached, s would
                  // definitely refer to a String (though
                  // with the given definition of foo, this
                  // line won't be reached...)

Массив определенно содержит объекты String даже после вызова foo. С другой стороны:

E[] ea = (E[]) new Object[5];
foo(ea);
E e = ea[0];  // e may now refer to a non-E object!

Метод foo мог бы вставить в массив объект не E. Поэтому, несмотря на то, что третья строка выглядит безопасно, первая (небезопасная) линия нарушила ограничения, гарантирующие безопасность.

Полный пример:

class Foo<E>
{
    void foo(Object [] oa) {
        oa[0] = new Object();
    }

    public E get() {
        E[] ea = (E[]) new Object[5];
        foo(ea);
        return ea[0];  // returns the wrong type
    }
}

class Other
{
    public void callMe() {
        Foo<String> f = new Foo<>();
        String s = f.get();   // ClassCastException on *this* line
    }
}

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

Кроме того, следующий код также неверен. Но почему? Кажется, что он хорошо работает и после стирания.

Код, о котором идет речь:

public class GenericArray<E>{
    E s= new E();
}

После стирания это будет:

Object s = new Object();

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

public <E> E getAnE() {
    return new E();
}

... потому что после стирания типа "новый E()" станет "новым объектом()" и возвращает объект не E из метода, явно нарушает его ограничения типа (предполагается, что он возвращает E) и поэтому является небезопасным. Если вышеуказанный метод должен был компилироваться, и вы его вызывали с помощью:

String s = <String>getAnE();

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

Дальнейшие примечания/пояснения:

  • Небезопасный (что не подходит для типа "небезопасно" ) означает, что он может потенциально вызвать ошибку типа времени выполнения в коде, которая в противном случае была бы звуковой. (Это на самом деле означает больше, чем это, но этого определения достаточно для целей этого ответа).
  • возможно вызвать ClassCastException или ArrayStoreException или другие исключения с "безопасным" кодом, но эти исключения происходят только в четко определенных точках. То есть вы обычно можете получить ClassCastException только при выполнении броска, операции, которая по своей сути несет этот риск. Аналогично, вы можете получить ArrayStoreException только при сохранении значения в массиве.
  • компилятор не проверяет, действительно ли такая ошибка произойдет, прежде чем она сообщит, что операция небезопасна. Он просто знает, что определенные операции потенциально могут вызвать проблемы и предупреждают об этих случаях.
  • что вы не можете создать новый экземпляр (или массив) параметра типа, является как языковой функцией, предназначенной для сохранения безопасности, так и, возможно, также отражать ограничения реализации, связанные с использованием стирания типа. То есть, new E() можно было бы создать экземпляр фактического параметра типа, когда на самом деле он мог создать только экземпляр стираемого типа. Разрешить его компиляцию было бы небезопасным и потенциально запутанным. В общем случае вы можете использовать E вместо фактического типа без какого-либо вреда, но это не так для экземпляра.

Ответ 2

Для объявлений Array требуется тип reifiable и дженерики не поддаются восстановлению.

Из документации: единственный тип, который вы можете разместить в массиве, - это тот, который можно повторно идентифицировать, то есть:

  • Он относится к объявлению не общего типа или интерфейса.

  • Это параметризованный тип, в котором все аргументы типа являются неограниченными подстановочными знаками (§4.5.1).

  • Это необработанный тип (§4.8).

  • Это примитивный тип (§4.2).

  • Это тип массива (§10.1), тип элемента которого можно повторно идентифицировать.

  • Это вложенный тип, где для каждого типа T, разделенного символом ".", сам T является reifiable.

Это означает, что единственное юридическое объявление для "общего" массива будет выглядеть как List<?>[] elements = new ArrayList[10];. Но это определенно не общий массив, это массив List неизвестного типа.

Основная причина, по которой Java жалуется на то, что вы выполняете трансляцию на E[], состоит в том, что она не отмечена. То есть вы переходите от проверенного типа явно к неконтролируемому; в этом случае проверенный общий тип E на непроверенный тип Object. Однако это единственный способ создать массив, который является общим, и обычно считается безопасным, если вам нужно использовать массивы.

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

Ответ 3

Компилятор может использовать переменную типа Object для выполнения любой переменной типа Cat. Компилятору, возможно, придется добавить приведение типов, но такой тип-метод либо выдаст исключение, либо даст ссылку на экземпляр Cat. Из-за этого сгенерированный код для SomeCollection<T> не должен фактически использовать какие-либо переменные типа T; компилятор может заменить T на Object и при необходимости переместить объекты, такие как возвращаемые значения функции, в T.

Компилятор не может использовать Object[], однако, сделать все, что может сделать Cat[]. Если a SomeCollection[] имел массив типа T[], он не смог бы создать экземпляр этого типа массива, не зная тип T. Он мог бы создать экземпляр Object[] и хранить ссылки на экземпляры T в нем, не зная тип T, но любая попытка сделать такой массив T[] гарантированно завершится неудачей, если T оказалось Object.

Ответ 4

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

Object[] myStrs = new Object[2];
myStrs[0] = 100;  // This is fine
myStrs[1] = "hi"; // Ambiguity! Hence Error.

Если пользователю разрешено создавать общий массив, тогда пользователь может сделать так, как я показал в приведенном выше коде, и он запутает компилятор. Он побеждает назначение массивов (Arrays can handle only same/similar/homogeneous type of elements, помните?). Вы можете всегда использовать массив класса/структуры, если вам нужен гетерогенный массив.

Подробнее здесь.