Как стать автором
Обновить

Комментарии 17

Можно завести правило, на пример, для SonarLint?

Неплохой вариант. И так как нужно проверять именно сигнатуру, кажется, такое правило не сложно будет создать. Спасибо за подсказку, нужно будет посмотреть в эту сторону.

Все аналогично древним математическим софизмам. На одном из этапов мы допускаем то, чего допускать нельзя и присваиваем что попало. JVM обмануть можно, но зачем, - чтобы обмануть себя?

Поддержу автора: люди сами себе злобные буратины.

Корень проблемы, насколько я понял, в особенной трактовке компилятором ограничения типа T extends U как T & U, хотя так делать нельзя, так как эти выражения не эквивалентны — во втором случае U может вообще не иметь к T никакого отношения. Интересно, по какой причине сделано так.

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

Не понимаю, какие проблемы в проверке интерфейсов? Компилятор же видит, что в конечном выражении (с левой стороны присваивания, например) используется класс, не реализующий интерфейс, требуемый ограничением типа. Почему же он позволяет такое скомпилировать?

он проверит, если класс финальный или у нас пересечение двух классов, так как множественное наследование классов запрещено. А если нет, то разницы с интерфейсом нет. Я пытался это объяснить в блоке «Переход на классы вместо интерфейсов».

На мой взгляд эта проблема связана как минимум с функционалом генерации класса на лету (в рантайме). И соответственно метод createResource может сгенерировать класс реализующий интерфейсы Resource и List<? extends Resource>. И по идее это вполне логично и допустимо. А это известно только в рантайме.

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

Мне кажется, что проблема скорее в том, что метод createResource() по своей сигнатуре возвращает некоторый тип T, единственное ограничение на который со стороны самого метода createResource() — он должен реализовывать Resource. Не очень понятно, как такое вообще трактовать.

Например, если считать, что generic-методы — это методы, которые работают для какого-то выбранного T, то есть вопрос: а кто "выбирает" тип T?

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

Если тип T выбирается вызывающим кодом, то получается, что метод должен уметь возвращать экземпляр произвольного типа T. Вызывающий код выбрал, какой T ему нужен (например, одновременно реализующий Resource и List<> — такие теоретически существуют), а метод должен этому удовлетворить. Что, разумеется, некоторый абсурд: метод даже не знает, что за T выбрал вызывающий код. Именно поэтому внутри тела метода приходится делать небезопасное преобразование к типу T — компилятор абсолютно правильно не может доказать, что FileResource является каким-то произвольным T.

Да, идея в том, что вот для такой сигнатуры метода

<T> T get();

тип <T>, определяет не метода, а вызывающий код.
И такая сигнатура

<T extends Runnable> T get();

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

Да, это выглядит очень странно, получается есть нелогичная конструкция в языке. Ведь мы не можем узнать какой тип хочет получить вызывающий код. Единственная возможность это использовать такую сигнатуру

<T> T get(Class<T> type);

А такой конструкции, вызывающий код должен нам сообщить, какой тип он хочет вернуть. Ну а если не сообщает, передавая null, это уже выстрел в ногу :) Именно поэтому, что такая сигнатура имеет хоть какой-то смысл, используется в библиотеках и является рекомендуемой в блоке "Указание типа в параметрах".

В Kotlin используется ключевое слово refied для типа, но это просто синтаксический сахар, который под капотом использует как раз Class<T>.

Тогда остаётся вопрос, почему же в случае с сигнатурой


<T extends Runnable> T get();

компилятор не бьёт по рукам при таком вызове


String s = get();

ведь он точно знает, что String не реализует интерфейс Runnable, так что это даже компилироваться не должно. И не важно, как там внутри устроен метод get().

Я вроде написал, что это похоже на баг. Если попробовать использовать cast, то скомпилировать код не получится, а вот автовывод типа работает. Это потому, что в документации это разные разделы. Автовывод появился только в 9 версии Java и видимо не учли этот момент в проработке компилятора. Мне кажется, даже сделать багфикс этого момента будет не сложно.

Классная статья! Тоже сталкивался с такой особенностью Java несколько лет назад. Я её хотел в тестах применить - типа избежать явного каста возвращаемого значения, что бы код чуть меньше был. Но в итоге всё равно оставили как есть как раз из за того что в коде можно переменной любого типа присвоить значение. Про передачу ожидаемого типа в качестве параметра не додумался - спасибо за инфу! А не могли бы вы привести ещё примеров, где этот подход используется, с передачей класса в параметре? А то я не примоню чтобы его встречал.

Спасибо большое за оценку. Очень рад, что статья понравилась.

Использование класса Class используется очень часто в различных библиотеках, там где нельзя заранее знать все случаи использования. В groove зачастую используется, в Spring Framework сплошь и рядом, в сериализаторах, наподобие Jackson. Уверен, если вы поищите в своем проекте через Intellij IDEA по области Scope - Class<,то найдёте не одно место использования.

Понятно. Спасибо!

Да самое простое — в JDK класс EnumSet и его фабрики значений.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий