Почему negate () требует явного приведения к Predicate? - PullRequest
8 голосов
/ 22 февраля 2020

У меня есть список имен. В строке 3 мне пришлось привести результат лямбда-выражения к Predicate<String>. В книге, которую я читаю, объясняется, что приведение необходимо для того, чтобы помочь компилятору определить соответствующий функциональный интерфейс.

Однако мне не нужно такое приведение в следующей строке, потому что я не Звоните negate(). Как это имеет значение? Я понимаю, что negate() здесь возвращает Predicate<String>, но разве предыдущее лямбда-выражение не делает то же самое?

List<String> names = new ArrayList<>();
//add various names here
names.removeIf(((Predicate<String>) str -> str.length() <= 5).negate()); //cast required
names.removeIf(((str -> str.length() <= 5))); //compiles without cast

Ответы [ 4 ]

8 голосов
/ 22 февраля 2020

Это не совсем только потому, что вы звоните negate(). Посмотрите на эту версию, которая очень близка к вашей, но компилируется:

Predicate<String> predicate = str -> str.length() <= 5;
names.removeIf(predicate.negate());

Разница между этой и вашей версией? Речь идет о том, как лямбда-выражения получают свои типы («целевой тип»).

Как вы думаете, что это делает?

(str -> str.length() <= 5).negate()?

Ваш текущий ответ таков: «он вызывает negate() на Predicate<String> определяется выражением str -> str.length() <= 5 ". Правильно? Но это только потому, что ты это хотел сделать. Компилятор не знает, что . Почему? Потому что это может быть что угодно. Мой собственный ответ на поставленный выше вопрос может быть следующим: «он вызывает negate для моего другого функционального типа интерфейса ... (да, пример будет немного странным)

interface SentenceExpression {
    boolean checkGrammar();
    default SentenceExpression negate() {
        return ArtificialIntelligence.contradict(explainSentence());
    };
}

Я мог бы использовать тот же самый лямбда-выражение names.removeIf((str -> str.length() <= 5).negate());, но означающее str -> str.length() <= 5 быть SentenceExpression, а не Predicate<String>.

Объяснение: (str -> str.length() <= 5).negate() не означает str -> str.length() <= 5 a Predicate<String>. И вот почему я сказал, что это может быть что угодно, включая мой функциональный интерфейс выше.

Назад к Java ... Вот почему лямбда-выражения имеют понятие «тип цели», которое определяет механику, с помощью которой лямбда-выражения понимается компилятором как данный тип функционального интерфейса (т. е. как вы помогаете компилятору узнать, что выражение является Predicate<String>, а не SentenceExpression или чем-то еще, что может быть). Вы можете найти полезным прочитать Что подразумевается под целевым типом лямбда и контекстом целевого типа в Java? и Java 8: типизация цели

Один из контекстов, в которых целевые типы вывод (если вы прочитайте ответы на эти посты) - это контекст вызова, где вы передаете лямбда-выражение в качестве аргумента для параметра типа функционального интерфейса, и это то, что применимо к names.removeIf(((str -> str.length() <= 5)));: это просто лямбда-выражение, данное в качестве аргумента для метод, который принимает Predicate<String>. Это не относится к оператору, который не компилируется.

Итак, другими словами ...

names.removeIf(str -> str.length() <= 5); использует лямбда-выражение в месте, где тип аргумента четко определяет, что ожидается, что тип лямбда-выражения будет (т. е. целевой тип str -> str.length() <= 5 явно Predicate<String>).

Однако (str -> str.length() <= 5).negate() не является лямбда-выражением, это просто выражение, которое происходит с используйте лямбда-выражение. Это означает, что str -> str.length() <= 5 в этом случае находится не в контексте вызова, который определяет целевой тип лямбда-выражения (как в случае вашего последнего оператора). Да, компилятор знает, что removeIf требуется Predicate<String>, и он точно знает, что все выражение, переданное методу, должно быть Predicate<String>, но он не будет предполагать, что любое лямбда-выражение в выражении аргумента будет Predicate<String> (даже если вы трактуете его как предикат, вызывая для него negate(); это может быть все, что совместимо с лямбда-выражением).

Вот почему вы набираете лямбду с помощью необходимо явное приведение (или иначе, как в первом контрпримере, который я дал).

4 голосов
/ 22 февраля 2020

Я не знаю, почему это так запутанно. Это можно объяснить двумя причинами: IMO.

  • Лямбда-выражения являются поли выражениями.

Я дам вам понять, что это значит, и что за слова JLS Но по сути это просто как дженерики:

static class Me<T> {
    T t...
}

что здесь типа T? Смотря как. Если вы делаете:

Me<Integer> me = new Me<>(); // it's Integer
Me<String>  m2 = new Me<>(); // it's String

поли-выражения говорят, что они зависят от контекста того, где они используются. Лямбда-выражения одинаковы. Давайте возьмем лямбда-выражение в изоляция здесь:

(String str) -> str.length() <= 5

, когда вы посмотрите на это, что это? Ну, это Predicate<String>? Но может быть A Function<String, Boolean>? Или может быть даже MyTransformer<String, Boolean>, где:

 interface MyTransformer<String, Boolean> {
     Boolean transform(String in){
         // do something here with "in"
     } 
 } 

Выбор бесконечен.

  • В теории .negate(), вызываемый напрямую , может быть вариантом .

С 10_000 миль и выше вы правы: вы предоставляете метод str -> str.length() <= 5 removeIf, который принимает только Predicate. removeIf методов больше нет, поэтому компилятор должен уметь «делать правильные вещи», когда вы предоставляете (str -> str.length() <= 5).negate().

Так почему же это не работает? Давайте начнем с вашего комментария:

Разве не нужно было вызывать negate (), чтобы предоставить еще больше контекста, делая явное приведение еще менее необходимым?

Кажется, это Именно здесь начинается основная проблема, просто не работает javac. Он не может взять весь str -> str.length() <= 5).negate(), сказать себе, что это Predicate<String> (поскольку вы используете его в качестве аргумента для removeIf), а затем разложить дальше часть без .negate() и посмотреть, является ли это Predicate<String> также. javac действует в обратном порядке, ему нужно знать цель , чтобы можно было определить, законно ли звонить negate или нет.

Также необходимо провести четкое различие между выражениями поли и выражениями в целом. str -> str.length() <= 5).negate() - это выражение, str -> str.length() <= 5 - это поли-выражение.

Могут быть языки, в которых все делается по-другому и где это возможно, javac просто не тот тип.

1 голос
/ 27 февраля 2020

Не вдаваясь в вещи слишком глубоко, суть в том, что лямбда str -> str.length() <= 5 не обязательно Predicate<String>, как объяснил Евгений .

То есть negate() это функция-член Predicate<T>, и компилятор не может использовать вызов negate(), чтобы помочь с выводом типа, который следует интерпретировать как лямбда. Даже если бы он попытался, могут быть проблемы, потому что, если у нескольких возможных классов есть функции negate(), он не будет знать, какой из них выбрать.

Представьте, что вы компилятор.

  • Вы видите str -> str.length() <= 5. Вы знаете, что это лямбда, которая принимает строку и возвращает логическое значение, но не знаете, какой именно тип она представляет.
  • Далее вы видите ссылку на член .negate(), но потому, что вы не знаете тип выражение слева от "." вам нужно сообщить об ошибке, так как вы не можете использовать вызов отрицания, чтобы помочь определить тип.

Если бы отрицание было реализовано как функция stati c, тогда все работало бы. Например:

public class PredicateUtils {
    public static <T> Predicate<T> negate(Predicate<T> p) {
        return p.negate();
}

позволит вам написать

names.removeIf(PredicateUtils.negate(str -> str.length() <= 5)); //compiles without cast

Поскольку выбор функции отрицания однозначен, компилятор знает, что ему нужно интерпретировать str -> str.length() <= 5 как Predicate<T> и может правильно привести тип.

Я надеюсь, что это поможет.

1 голос
/ 23 февраля 2020

В следующем примере

      names.removeIf(str -> str.length() <= 5); //compiles without cast

выражение возвращает true. Если в следующем примере без приведения true ничего не знает о методе negate()

С другой стороны,

   names.removeIf(((Predicate<String>) str -> str.length() <= 5).negate()); //cast required

Выражение приведено к Predicate<String>, чтобы сообщить компилятор, где найти метод negate. Тогда это метод negate(), который на самом деле выполняет эвакуацию следующим образом:

   (s)->!test(s) where s is the string argument

Обратите внимание, что вы можете достичь того же результата без приведения, как указано ниже:

    names.removeIf(str->!str.length <= 5) 
      // or
    name.removeIf(str->str.length > 5)

...