Почему потребители принимают лямбда с органами выписки, но не являются органами выражения?
Следующий код на удивление успешно компилируется:
Consumer<String> p = ""::equals;
Это тоже:
p = s -> "".equals(s);
Но это ошибка с ошибкой boolean cannot be converted to void
, как ожидалось:
p = s -> true;
Модификация второго примера с круглыми скобками также не выполняется:
p = s -> ("".equals(s));
Является ли это ошибкой в компиляторе Java или существует ли правило вывода типа, о котором я не знаю?
Ответы
Ответ 1
Во-первых, стоит посмотреть, что такое Consumer<String>
. Из документации:
Представляет операцию, в которой принимает один входной аргумент и не возвращает результат. В отличие от большинства других функциональных интерфейсов, Consumer как ожидается, будет работать с помощью побочных эффектов.
Итак, это функция, которая принимает строку и ничего не возвращает.
Consumer<String> p = ""::equals;
Скомпилируется успешно, потому что equals
может принимать строку (и, действительно, любой объект). Результат равных просто игнорируется. *
p = s -> "".equals(s);
Это точно то же самое, но с другим синтаксисом. Компилятор не знает, чтобы добавить неявный return
, потому что Consumer
не должен возвращать значение. Он добавил бы неявный return
, если лямбда была Function<String, Boolean>
, хотя.
p = s -> true;
Это принимает строку (s
), но поскольку true
является выражением, а не выражением, результат нельзя игнорировать одинаково. Компилятор должен добавить неявный return
, потому что выражение не может существовать самостоятельно. Таким образом, у этого есть возврат: булев. Поэтому это не a Consumer
. **
p = s -> ("".equals(s));
Опять же, это выражение, а не утверждение. Игнорируя ламбы на мгновение, вы увидите, что строка System.out.println("Hello");
будет аналогичным образом не скомпилироваться, если вы завернете ее в круглые скобки.
* Из спецификация:
Если тело лямбда является выражением оператора (то есть выражением, которое можно было бы оставить в силе как оператор), оно совместимо с типом функции, производящей пустоты; любой результат просто отбрасывается.
** Из спецификация (спасибо, Eugene):
Лямбда-выражение конгруэнтно с функцией функции [void-production], если... тело лямбда представляет собой выражение выражения (§14.8) или совместимый с void блок.
Ответ 2
Я думаю, что другие ответы затрудняют объяснение, сосредотачиваясь на lambdas, тогда как их поведение в этом случае похоже на поведение методов, реализованных вручную. Это компилируется:
new Consumer<String>() {
@Override
public void accept(final String s) {
"".equals(s);
}
}
тогда как это не означает:
new Consumer<String>() {
@Override
public void accept(final String s) {
true;
}
}
потому что "".equals(s)
- это оператор, а true
- нет. Выражение лямбда для функционального интерфейса, возвращающего void, требует выражения, так что оно следует тем же правилам, что и тело метода.
Обратите внимание, что в целом тела лямбда не соответствуют точно таким же правилам, как тела метода - в частности, если лямбда, тело которой является выражением, реализует метод, возвращающий значение, он имеет неявный return
. Так, например, x -> true
будет допустимой реализацией Function<Object, Boolean>
, тогда как true;
не является допустимым телом метода. Но в этом конкретном случае функциональные интерфейсы и тела методов совпадают.
Ответ 3
s -> "".equals(s)
и
s -> true
не полагаются на одинаковые дескрипторы функций.
s -> "".equals(s)
может ссылаться на дескриптор функции String->void
или String->boolean
.
s -> true
относится только к дескриптору функции String->boolean
.
Почему?
- когда вы пишете
s -> "".equals(s)
, тело лямбда: "".equals(s)
- это оператор, создающий значение.
Компилятор считает, что функция может возвращать либо void
, либо boolean
.
Итак, пишу:
Function<String, Boolean> function = s -> "".equals(s);
Consumer<String> consumer = s -> "".equals(s);
.
Когда вы назначаете тело лямбда объявленной переменной Consumer<String>
, используется дескриптор String->void
.
Конечно, этот код не имеет большого смысла (вы проверяете равенство и не используете результат), но компилятору все равно.
Это то же самое, когда вы пишете оператор: myObject.getMyProperty()
где getMyProperty()
возвращает значение boolean
, но не сохраняет его результат.
- когда вы пишете
s -> true
, тело лямбда: true
- это одно выражение.
Компилятор считает, что функция возвращает обязательно boolean
.
Таким образом, может использоваться только дескриптор String->boolean
.
Теперь вернитесь к своему коду, который не компилируется.
Что вы пытаетесь сделать?
Consumer<String> p = s -> true;
Вы не можете. Вы хотите назначить переменную, которая использует дескриптор функции Consumer<String>
тела лямбды с дескриптором функции String->void
.
Это не соответствует!