Комментарии 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<
,то найдёте не одно место использования.
Как устроен вывод Generic-типов в Java