Pull to refresh

Comments 39

Я в Java не разбираюсь, но разве MyClass не должен быть параметрическим, или определять параметр T базового класса?

А то куда-то параметр T при наследовании пропал.
Пример вымышленный. В реальном коде наследник будет параметризованным. Но здесь параметризация опущена умышленно, чтобы раскрыть проблему.
В целом, не параметризовать наследника — это не ошибка.
Дженерики в Java создавались с учетом обратной совместимости. Раньше был просто Collection, потом создали generics и появился Collection. Но те, у кого старый код, вполне могли создать своего наследника MyCollection extends SomeCollection (где SomeCollection параметризован ). Т.к. у нас есть обратная совместимость, то старый код не свалится, просто параметризация Collection будет для него проигнорирована.
То есть Java инициирует все пропущенные генерики параметров классом Object?

Забавно.
Формально — да. В реальности — «generics в Java не существует» :)
А с чего такие выводы, что не существует? Дело в том, что в рантайме ничего не мешает определить какой именно тип используется у класса как параметризатор для дженерикс. Или вы имеете в виду, что в Listв рантайме ничего не мешает положить Integer?
Да, ничто не мешает положить туда Integer, это сделано опять же в целях совместимости.

(Старые программы могли использовать эту особенность)
Имеется в виду второе. List «ничего не знает» о том, что же в нем лежит (для каких элементов он предназначен). Если компилятор не проверил то, что кладется в List (или просто не имел возможности это сделать) на этапе компиляции, то мы можем получить ClassCastException в неожиданных местах.
На самом деле это не важно. В данном случае мы получаем эксепшн при взятии не правильного элемента из коллекции, а если бы в рантайме тип коллекции проверялся, то мы бы получали эксепшн при попытке положить. Т.е. все сводится к месту где мы замечаем проблему. Кстати в commons-collections есть способы «затипизировать» коллекции таким образом, что будет эксепшн при попытке положить в рантайме.
Закралась опечатка. Должно быть:
«Раньше был просто Collection, потом создали generics и появился Collection<E>»
Приняли за html =)
абсолютно не должен быть. даже более — это очень частая ситуация, когда интерфейсы параметризируются со временем, а все имплементирующие классы никто трогать не будет
Issue by design :)

А именно: generic'и в java реализованы поверх jvm, на уровне компилятора чуть ли не синтаксической заменой; никакой информации в рантайме о том, каким типом был параметризован generic, не поддерживается. Соответственно, Class, параметризованный наиболее общим типом Object, после компиляции будет заменен на Class (будет эквивалентен просто типу Class). Именно такой реализации вы не предоставили (напомню, что классы компилируются отдельно друг от друга, и потому MyClass требует точной ссылки на Class, а не на Class
… Почему-то сожрало остаток фразы.

Так вот, кстати, Netbeans это понимает и предлагает заменить Class
>параметризованный наиболее общим типом Object
Не совсем понял, причем тут именно Object. В данном примере можно заменить Class на любой другой параметризованный класс, а Object на любой объект. У меня в изначальном примере было Collection, но я хотел вырезать импорты чтобы сделать пост покороче, поэтому взял все классы из java.lang :).
Насчет рантайма согласен, но: как связана параметризация метода test и параметризация класса BaseClass?
Достоверно известно, что пример будет работать, если:
1. Добавить параметризацию при наследовании (напр. MyClass extends BaseClass)
2. Убрать параметризацию BaseClass.
3. Убрать дженерики из параметров метода test (т.е. оставить test(Class clazz)).
Мне непонятно почему параметризация BaseClass приводит к ошибке.
Опять забыл что угловые скобки съедаются как html, пардон.
Правильно читать: «в изначальном примере было Collection<String>» и «напр. MyClass extends BaseClass<String>»
Думаю, что мне отвечать уже не нужно, потому что Вы ниже ответили сами в дискуссии с tegger :) Именно type erasure при компиляции заставляет забыть о том, что тип параметризуем, и требовать отсылки к уже «развернутой» версии типа.
Насчёт «никакой информации в рантайме» вы неправы. Как раз в случае наследования от параметризованных классов информация о параметризации предка имеется и может быть вытащена через reflection.
Вы не правы. Сохраняется только информация о конкретной параметризации полей, методов и их аргументов
Я говорил про getGenericSuperclass(). А вот что бы сохранялись типы полей, методов и аргументов — это новость… Как же Type Erasure?
Применимо только для конкретной параметризации (когда она ясна на этапе компиляции). Тоесть, если ваш класс параметризуем (а не параметризован), то информацию о парметризации нельзя достать в рантайме.
Наверное вы просто не пробовали:) А вот у меня код отлично в рантайме определяет генерикс типы классов.
Можно достать информацию о параметризации конкретного аргумента/поля/результата функции. То есть если есть функция

List<String> doSomething()

То через reflection можно получить информацию о том, чем параметризирован List в данном конкретном случае.

Но если просто получать Class.forName(«List»), то, разумеется, никакой информации о параметризации мы не получим.
Поскольку родовые типы реализуются практически полностью в компиляторе Java, а не в библиотеке время исполнения, практически вся информация о родовых типах «стирается» при генерации байткода. Другими словами, компилятор генерирует почти такой же код, который вы написали бы вручную без использования родовых типов и приведений типов, после проверки вашей программы на независимость от типа. В отличие от С++ List и List являются одним и тем же классом (хотя и имеют различные типы, являющиеся подтипами List — отличие более важное в JDK 5.0, чем в предыдущих версиях языка).

Одним из побочных эффектов механизма стирания является неспособность класса реализовать оба интерфейса Comparable и Comparable, поскольку оба они на самом деле являются одним и тем же интерфейсом, указывая на один и тот же метод compareTo(). Может показаться более благоразумным объявить класс DecimalString, совместимый как со Strings, так и с Numbers, но с точки зрения компилятора Java вы пытались бы объявить один и тот же метод дважды:

public class DecimalString implements Comparable, Comparable {… } // нет

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

public T naiveCast(T t, Object o) { return (T) o; }

Компилятор просто выдаст не отмеченное предупреждение о преобразовании, поскольку не знает о том, является приведение типов безопасным или нет. Метод naiveCast() фактически не выполняет какого-либо приведения типов. T просто замещается его «стирателем» (Object), а переданный объект будет приведен в Object — не то что задумывалось.

Механизм стирания также ответственен за описанные выше проблемы создания – вы не можете создать объект родового типа, поскольку компилятор не знает, какой конструктор вызвать. Если ваш родовой класс требует создания объектов, чей тип указан параметрами родового типа, его конструкторы должны принимать литерал класса (Foo.class) и сохранять его, для того чтобы экземпляры могли быть созданы отображением.
Поскольку родовые типы реализуются практически полностью в компиляторе Java, а не в библиотеке время исполнения, практически вся информация о родовых типах «стирается» при генерации байткода. Другими словами, компилятор генерирует почти такой же код, который вы написали бы вручную без использования родовых типов и приведений типов, после проверки вашей программы на независимость от типа. В отличие от С++ List<Integer> и List<String> являются одним и тем же классом (хотя и имеют различные типы, являющиеся подтипами List<?> — отличие более важное в JDK 5.0, чем в предыдущих версиях языка).

Одним из побочных эффектов механизма стирания является неспособность класса реализовать оба интерфейса Comparable<String> и Comparable<Number>, поскольку оба они на самом деле являются одним и тем же интерфейсом, указывая на один и тот же метод compareTo(). Может показаться более благоразумным объявить класс DecimalString, совместимый как со Strings, так и с Numbers, но с точки зрения компилятора Java вы пытались бы объявить один и тот же метод дважды:

public class DecimalString implements Comparable<Number>, Comparable<String> {… } // нет

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

public <T> T naiveCast(T t, Object o) { return (T) o; }

Компилятор просто выдаст не отмеченное предупреждение о преобразовании, поскольку не знает о том, является приведение типов безопасным или нет. Метод naiveCast() фактически не выполняет какого-либо приведения типов. T просто замещается его «стирателем» (Object), а переданный объект будет приведен в Object — не то что задумывалось.

Механизм стирания также ответственен за описанные выше проблемы создания – вы не можете создать объект родового типа, поскольку компилятор не знает, какой конструктор вызвать. Если ваш родовой класс требует создания объектов, чей тип указан параметрами родового типа, его конструкторы должны принимать литерал класса (Foo.class) и сохранять его, для того чтобы экземпляры могли быть созданы отображением.
К чему весь этот текст? :)

Да, описанные проблемы имеют место быть. Но информацию о том, какую параметризацию имеет, например, List, выступающий в качестве типа поля класса, можно.

P.S.: А копирайт ставить надо бы:

Это наследование от type erasure (от BaseClass без параметров). А внутри type erasure не используются параметризованные типы.
Правильно я понимаю, что Вы имеете в виду: если наследоваться без параметров, то компилятор «забывает» любую параметризацию внутри класса BaseClass, поэтому он считает что я пытаюсь реализовать метод test с типизированными параметрами, тогда как надо реализовывать просто test(Class clazz)?
Если так, то ситуация понятна, но это не нормальное поведение, т.к. нарушается обратная совместимость. Допустим у меня есть Collection<E> и MyArrayList<E> и внутри MyArrayList есть параметризованный абстрактный метод. А мой клиент отнаследовался от MyArrayList когда в нем еще ничего не было параметризовано. Получается в новой версии код клиента сломается, т.е. в механизме type erasure есть проблемы с обратной совместимостью… Или я что-то упускаю?
Я понял подвох собственного вопроса. Если код наследника старый, то и метод не будет параметризован, а тогда все в порядке.
И все же, получается, если я обрезаю параметризацию у класса, то автоматом обрезается и параметризация внутри его методов? Это как-то неправильно, хотя и документированно.
Я бы сказал так: все в порядке, но поведение компилятора в этой ситуации могло бы быть более умным. Ошибка не вполне соответствует проблеме, хотя почему она такая — теперь понятно.
Компилятор, похоже, считает, что программер выучил language specification наизусть:) Приходится держать эту ценную книжку под рукой.
Ну да, так и есть. По сути, тут наследование не от исходного BaseClass, а от другого, raw-типа со стертыми параметрами. В raw-классе параметры типов стираются еще и у его собственных методов, полей и конструкторов. Поэтому абстрактный метод в этом raw-типе выглядит как test(Class clazz), и реализовывать надо именно такой метод.
Насчет обратной совместимости — тоже правильно, эту упячку придумали для того, чтобы можно было параметризовывать старый код, не затрагивая клиентов этого кода.
Вроде все нормально. В 8.4.2 приводится пример наследования от обыкновенного, не-raw-типа с generic-методом. В посте — наследование от raw-типа со стертыми сигнатурами всех методов.
Вдогонку к предыдущему комментарию:
(Notion of subsignature) allows a method whose signature does not use generic types to override any generified version of that method. А с MyClass получается наоборот — мы пытаемся generified-версией метода переопределить raw-версию.
Вы не правы, что это чисто академический интерес представляет. Я похоже пару раз пересекался с вариациями на это поведение в реальных проектах. Но никогда не углублялся в расследования.
Вспомнил, наконец, где я видел логин amima. Привет от екатеринбуржского я.офиса:)
Привет из Питера! :)
И спасибо за разъяснения.
Sign up to leave a comment.

Articles