Проблемы с пониманием нижних границ при использовании с лямбдой и функциональным интерфейсом

При изучении потоков 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.