Ответ 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
вместо фактического типа без какого-либо вреда, но это не так для экземпляра.