Ответ 1
Обновить: этот ответ получил больше внимания и внимания, чем я думаю, что он заслуживает того, чтобы в основном копировать исходный код JDK, поэтому я попытаюсь превратить его в нечто достойное.
Генераторы Java предназначены для того, чтобы выглядеть и чувствовать себя как настоящие, reified, мульти-инстанцированные, С++ или С# -типы. Это означает, что для такого типа, как ArrayList<E>
, мы ожидаем, что ArrayList<String>
будет вести себя так, как если бы каждое вхождение E
было заменено на String
. Другими словами, это:
private Object[] elementData = new Object[size];
public E get(int i) {
return (E) elementData[i];
}
String str = list.get(0);
должно стать следующим:
private Object[] elementData = new Object[size];
public String get(int i) {
return (String) elementData[i];
}
String str = list.get(0);
Теперь, как вы, наверное, знаете, это не так, как они работают. Для обратных совместимых причин, которые сейчас (в основном) давно позади нас, Java-генерики реализуются с помощью стирания типов, где E
фактически заменяется на Object
всюду, а приведения к String
вставляются в вызывающий код, где это необходимо, Это означает, что код действительно становится примерно таким:
private Object[] elementData = new Object[size];
public Object get(int i) {
return elementData[i];
}
String str = (String) list.get(0);
Приведение к (E)
исчезло и снова появилось на сайте вызова. Если сайт вызова проигнорировал результат, бросок исчез бы полностью! Вот почему он дал "непроверенное" предупреждение.
Теперь представьте, если вместо elementData
был тип E[]
, как вы предлагаете. То есть код выглядит следующим образом:
private E[] elementData = (E[]) new Object[size];
public E get(int i) {
return elementData[i];
}
String str = list.get(0);
Мы знаем, что он трансформируется в то же самое, что и выше, из-за стирания. Но если бы мы описали дженерики, как мы этого хотели, это выглядело бы так:
private String[] elementData = (String[]) new Object[size];
// ClassCastException: Object[] is not a String[]
По сути, мы написали код, который должен быть поврежден во время выполнения, и единственная причина, по которой он вообще работает, заключается в том, что реализация генерических программ Java делает вид, что она лучше, чем есть. Мы солгали компилятору, чтобы убедить его принять хрупкий код.
И это хрупкое! Мы избегаем сбоев во время выполнения, потому что массив никогда не ускользает от класса. Но если бы это произошло, это вызвало бы ClassCastException
в трудно предсказанных местах. Что делать, если Java 9 представила обновленные дженерики? Первая реализация продолжила бы работать, но это сломалось бы.
Вот почему большинство разумных соглашений о кодировании Java требуют, чтобы неконтролируемые отбрасывания были правильными по типу. (E) elementData[i]
является корректным, потому что ArrayList
гарантирует, что в elementData
можно сохранить только E
. (E[]) new Object[size]
никогда не будет корректным, если E
не является Object
.
Есть и другие преимущества. В Java 8 поле elementData
может принимать специальные значения дозорного значения:
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access