Проблемы с пониманием нижних границ при использовании с лямбдой и функциональным интерфейсом
При изучении потоков Java8 я наткнулся на следующий фрагмент кода:
Predicate<? super String> predicate = s -> s.startsWith("g");
Поскольку общий параметр является нижней границей, я решил, что это не скомпилируется. То, как я это вижу, если объект является супертипом для String, то передача объекта типа должна его разорвать, так как Object не имеет функции startWith(). Однако я был удивлен, увидев, что он работает без проблем.
Далее, когда я изменил предикат, чтобы взять верхнюю границу:
<? extends String>,
он не будет компилироваться.
Я думал, что понял смысл верхней и нижней границ, но, очевидно, я что-то упускаю. Может ли кто-нибудь объяснить, почему нижняя граница работает с этой лямбдой?
Ответы
Ответ 1
Тип аргумента Lambda является точным, он не может быть ? super
или ? extends
. Это описано в JLS 15.27.3. Тип выражения лямбда. Он вводит концепцию наземного целевого типа (в основном это лямбда-тип). Среди прочего он заявил, что:
Если T
- это тип функционального интерфейса с параметром подстановочного знака, и выражение лямбда неявно набирается, тогда целевой целевой тип - это несимметричная параметризация (§9.9) T.
Акцент мой. Так что, когда вы пишете
Predicate<? super String> predicate = s -> s.startsWith("g");
Ваш лямбда-тип Predicate<String>
. Это то же самое, что:
Predicate<? super String> predicate = (Predicate<String>)(s -> s.startsWith("g"));
Или даже
Predicate<String> pred = (Predicate<String>)(s -> s.startsWith("g"));
Predicate<? super String> predicate = pred;
Учитывая тот факт, что аргументы типа lambdas являются конкретными, после этого применяются правила нормального преобразования типов: Predicate<String>
- это Predicate<? super String>
или Predicate<? extends String>
. Поэтому должны компилироваться как Predicate<? super String>
, так и Predicate<? extends String>
. И оба на самом деле работают для меня на javac 8u25, 8u45, 8u71, а также ecj 3.11.1.
Ответ 2
Я только что протестировал его, само задание компилируется. Какие изменения вы можете называть predicate.test()
.
Вернемся назад и используйте пояснение GenericClass<T>
для объяснения. Для аргументов типа Foo
extends Bar
и Bar
extends Baz
.
Расширяет:
Когда вы объявляете GenericClass<? extends Bar>
, вы говорите: "Я не знаю, каков его общий аргумент типа, но он является подклассом Bar
". Фактический экземпляр всегда будет иметь аргумент типа без подстановочных знаков, но в этой части кода вы не знаете, что это за значение. Теперь рассмотрим, что это значит для вызовов методов.
Вы знаете, что вы на самом деле получили либо GenericClass<Foo>
, либо GenericClass<Bar>
. Рассмотрим метод, возвращающий T
. В первом случае его тип возврата Foo
. В последнем случае Bar
. В любом случае это подтип Bar
и безопасно назначить переменной Bar
.
Рассмотрим метод, который имеет параметр T
. Если это a GenericClass<Foo>
, то передача его Bar
является ошибкой - Bar
не является подтипом Foo
.
Итак, с верхней границей вы можете использовать общие возвращаемые значения, но не общие параметры метода.
Super:
Когда вы объявляете GenericClass<? super Bar>
, вы говорите: "Я не знаю, каков его общий аргумент типа, но это суперкласс из Bar
". Теперь рассмотрим, что это значит для вызовов методов.
Вы знаете, что вы на самом деле получили либо GenericClass<Bar>
, либо GenericClass<Baz>
. Рассмотрим метод, возвращающий T
. В первом случае он возвращает Bar
. В последнем, Baz
. Если он возвращает Baz
, то присвоение этого значения переменной Bar
является ошибкой. Вы не знаете, что это такое, поэтому вы не можете без всякой надобности принять что-либо здесь.
Рассмотрим метод, который имеет параметр T
. Если это a GenericClass<Bar>
, то передача его a Bar
является законной. Если это a GenericClass<Baz>
, то передача его a Bar
по-прежнему является законной, поскольку Bar
является подтипом Baz
.
Итак, с нижней границей вы можете использовать общие параметры метода, но не общие возвращаемые значения.
Вкратце: <? extends T>
означает, что вы можете использовать общие возвращаемые значения, но не параметры. <? super T>
означает, что вы можете использовать общие параметры, но не возвращать значения. predicate.test()
имеет общий параметр, поэтому вам нужно super
.
Еще одна вещь, которую следует учитывать. Оценки, указанные подстановочным знаком, касаются фактического аргумента типа объекта. Их последствия для типов, которые вы можете использовать с этим объектом, противоположны. Подстановочный знак верхней границы (extends
) является нижней границей типов переменных, которые вы можете присвоить значениям возврата. Подстановочный знак нижней границы (super
) является верхней границей типов, которые вы можете передать в качестве параметров. predicate.test(new Object())
не будет компилироваться, потому что с нижней границей String
он будет принимать только подклассы String
.