Как работают дженерики?

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

ArrayList<Quod> test=new ArrayList<Quod>();
ArrayList obj=new ArrayList();
test=obj;
obj.add(new Object());

System.out.println(test.get(0));

Итак, в основном, я добавляю Object в ArrayList of Quods. Теперь я вижу, как у java нет возможности эффективно проверять это, потому что ему придется просматривать все ссылки, которые, вероятно, даже не хранятся нигде. Но почему это работает get(). Не кажется ли get() возвращать экземпляр Quod, как говорится, когда вы наводите на него курсор в Eclipse? Если он может вернуть объект, который является только объектом, когда он обещал вернуть объект типа Quod, почему я не могу вернуть String, когда я говорю, что верну int?

И все становится еще страннее. Это приведет к сбою, поскольку предполагается, что с ошибкой во время выполнения (ошибка java.lang.ClassCastException) (!?!?):

ArrayList<Quod> test=new ArrayList<Quod>();
ArrayList obj=new ArrayList();
test=obj;
obj.add(new Object());

System.out.println(test.get(0).toString());

Почему я не могу вызвать toString для объекта? И почему это прекрасно для метода println() для вызова его toString, но не для меня непосредственно?


EDIT: Я знаю, что я ничего не делаю с первым экземпляром ArrayList, который я создаю, поэтому это просто пустая трата времени обработки.


EDIT: Я использую Eclipse на Java 1.6. Другие сказали, что они получают одинаковые результаты в Eclipse, работающем с java 1.8. Однако на некоторых других компиляторах в обоих случаях возникает ошибка CCE.

Ответы

Ответ 1

Генераторы Java реализуются посредством стирания типа, т.е. аргументы типа используются только для компиляции и связывания, но стираются для выполнения. То есть, нет соответствия 1:1 между типами времени компиляции и типами времени выполнения. В частности, все экземпляры родового типа имеют один и тот же класс выполнения:

new ArrayList<Quod>().getClass() == new ArrayList<String>().getClass();

В системе типа времени компиляции присутствуют аргументы типа и используются для проверки типов. В системе типа времени выполнения аргументы типа отсутствуют и поэтому не проверяются.

Это не проблема, а для типов и типов. Приведение является утверждением правильности типа и отбрасывает проверку типа от времени компиляции до времени выполнения. Но, как мы видели, нет соответствия 1:1 между временем компиляции и типами времени выполнения; аргументы типа стираются во время компиляции. Таким образом, среда выполнения не может полностью проверить правильность приведений, содержащих параметры типа, а неправильное выполнение может преуспеть, нарушая систему типа времени компиляции. Спецификация Java Language Specification вызывает это загрязнение кучи.

Как следствие, среда выполнения не может полагаться на правильность аргументов типа. Тем не менее, он должен обеспечить целостность системы типа времени выполнения, чтобы предотвратить повреждение памяти. Это достигается путем задержки проверки типа до тех пор, пока фактическая ссылка не будет использована, и в этот момент среда выполнения знает способ или область, которые она должна поддерживать, и может проверить, что она фактически является экземпляром класса или интерфейса, который объявляет это поле или метод.

С этим вернемся к вашему примеру кода, который я немного упростил (это не меняет поведения):

ArrayList<Quod> test = new ArrayList<Quod>();
ArrayList obj = test; 
obj.add(new Object());
System.out.println(test.get(0));

Объявленный тип obj является необработанным типом ArrayList. Необработанные типы отключают проверку аргументов типа во время компиляции. Как следствие, мы можем передать Object в свой метод добавления, хотя ArrayList может содержать только экземпляры Quod в системе типа времени компиляции. То есть, мы успешно совладели с компилятором и сделали загрязнение кучи.

Это оставляет систему типа времени выполнения. В системе типа времени выполнения ArrayList работает со ссылками типа Object, поэтому передача метода Object в метод add выполняется нормально. Так вызывается get(), который также возвращает Object. И вот все расходится: в вашем первом примере кода у вас есть:

System.out.println(test.get(0));

Тип времени компиляции test.get(0) равен Quod, единственным подходящим методом println является println(Object), и поэтому это подпись метода, встроенная в файл класса. Поэтому во время выполнения мы передаем Object методу println(Object). Это совершенно нормально, и, следовательно, исключение не выбрасывается.

В вашем втором примере кода у вас есть:

System.out.println(test.get(0).toString());

Опять же, тип времени компиляции test.get(0) равен Quod, но теперь мы вызываем его метод toString(). Поэтому компилятор указывает, что метод toString, объявленный в (или унаследованный) тип Quod, должен быть вызван. Очевидно, для этого метода требуется this указать экземпляр Quod, поэтому компилятор добавляет дополнительный приведение к Quod в код байта до вызова метода - и этот бросок бросает a ClassCastException.

Таким образом, среда выполнения разрешает первый пример кода, потому что ссылка не используется способом, специфичным для Quod, но отклоняет второе, потому что ссылка используется для доступа к методу типа Quod.

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

Ответ 2

Суть вопроса:

И почему это прекрасно для метода println() для вызова его toString, но не для меня непосредственно?

ClassCastException исключение не возникает из-за вызова toString(), а из-за явного добавления, добавленного компилятором.

Изображение стоит тысячи слов, поэтому давайте посмотрим на некоторый декомпилированный код.

Рассмотрим следующий код:

public static void main(String[] args) {
    List<String> s = new ArrayList<String>();
    s.add("kshitiz");
    List<Integer> i = new ArrayList(s);

    System.out.println(i.get(0)); //This works
    System.out.println(i.get(0).toString()); // This blows up!!!
}

Теперь рассмотрим декомпилированный код:

public static void main(String[] args) {
    ArrayList s = new ArrayList();
    s.add("kshitiz");
    ArrayList i = new ArrayList(s);
    System.out.println(i.get(0));
    System.out.println(((Integer)i.get(0)).toString());
}

См. явное приведение к Integer? Теперь почему компилятор не добавил листинг в предыдущей строке? Подпись метода println():

public void println(Object x)

Так как println ожидает Object, а результат i.get(0) равен Object, добавление cast не добавляется.

Также вы можете вызывать toString(), если вы делаете это так, чтобы не генерировались никакие кавычки:

public static void main(String[] args) {
    List<String> s = new ArrayList<String>();
    s.add("kshitiz");
    List<Integer> i = new ArrayList(s);

    myprint(i.get(0));
}

public static void myprint(Object arg) {
    System.out.println(arg.toString()); //Invoked toString but no exception
}