Почему эта лямбда Java 8 не компилируется?

85

Следующий код Java не компилируется:

@FunctionalInterface
private interface BiConsumer<A, B> {
    void accept(A a, B b);
}

private static void takeBiConsumer(BiConsumer<String, String> bc) { }

public static void main(String[] args) {
    takeBiConsumer((String s1, String s2) -> new String("hi")); // OK
    takeBiConsumer((String s1, String s2) -> "hi"); // Error
}

Компилятор сообщает:

Error:(31, 58) java: incompatible types: bad return type in lambda expression
    java.lang.String cannot be converted to void

Странно то, что строка с пометкой «ОК» компилируется нормально, а строка с пометкой «Ошибка» не работает. Они кажутся практически идентичными.

Брайан Гордон
источник
5
это опечатка, что метод функционального интерфейса возвращает void?
Натан Хьюз
6
@NathanHughes Нет. Оказывается, это центральный вопрос - см. Принятый ответ.
Брайан Гордон,
должен ли код внутри { }из takeBiConsumer... и если да, то не могли бы вы привести пример ... если бы я прочитал это правильно, bcявляется экземпляром класса / интерфейса BiConsumer, и , таким образом , должен содержать метод , называемый acceptв соответствии с интерфейсом подпись. .. ... и если это верно, то acceptметод должен быть где-то определен (например, класс, реализующий интерфейс) ... так вот что должно быть в {}?? ... ... ... спасибо
dsdsdsdsd
Интерфейсы с одним методом взаимозаменяемы с лямбдами в Java 8. В данном случае (String s1, String s2) -> "hi"это экземпляр BiConsumer <String, String>.
Брайан Гордон,

Ответы:

100

Ваша лямбда должна соответствовать BiConsumer<String, String>. Если вы обратитесь к JLS # 15.27.3 (Тип лямбда) :

Лямбда-выражение конгруэнтно типу функции, если выполняются все следующие условия:

  • [...]
  • Если результат типа функции void, лямбда-тело является либо выражением инструкции (§14.8), либо блоком, совместимым с void.

Таким образом, лямбда должна быть либо выражением оператора, либо блоком, совместимым с void:

  • Вызов конструктора - это выражение оператора, поэтому оно компилируется.
  • Строковый литерал не является выражением оператора и несовместим с void (см. Примеры в 15.27.2 ), поэтому он не компилируется.
ассилий
источник
31
@BrianGordon Строковый литерал - это выражение (точнее, константное выражение), но не выражение оператора.
assylias
44

По сути, new String("hi")это исполняемый фрагмент кода, который действительно что-то делает (создает новую строку и затем возвращает ее). Возвращаемое значение можно игнорировать и по- new String("hi")прежнему использовать в лямбда-выражении void-return для создания новой строки.

Однако "hi"это просто константа, которая сама по себе ничего не делает. Единственное, что с ним можно сделать в теле лямбда - это вернуть его. Но лямбда-метод должен иметь возвращаемый тип Stringили Object, но он возвращает void, отсюда и String cannot be casted to voidошибка.

Kajacx
источник
6
Правильный формальный термин - это выражение выражения , выражение создания экземпляра может появляться в обоих местах, где требуется выражение или где требуется оператор, в то время как Stringлитерал - это просто выражение, которое нельзя использовать в контексте оператора .
Holger
2
Принятый ответ может быть формально правильным, но это лучшее объяснение
edc65
3
@ edc65: вот почему и за этот ответ проголосовали. Обоснование правил и неформальное интуитивное объяснение действительно могут помочь, однако каждый программист должен знать, что за этим стоят формальные правила, и в случае, если результат формального правила не интуитивно понятен, формальное правило все равно будет иметь преимущество. . Например, ()->x++это законно, в то время как ()->(x++), в основном, делать то же самое, не является…
Хольгер
21

В первом случае все в порядке, потому что вы вызываете «специальный» метод (конструктор) и фактически не берете созданный объект. Чтобы было понятнее, я добавлю в лямбды необязательные фигурные скобки:

takeBiConsumer((String s1, String s2) -> {new String("hi");}); // OK
takeBiConsumer((String s1, String s2) -> {"hi"}); // Error

И, что более ясно, я переведу это на более старые обозначения:

takeBiConsumer(new BiConsumer<String, String>(String s1, String s2) {
    public void accept(String s, String s2) {
        new String("hi"); // OK
    }
});

takeBiConsumer(new BiConsumer<String, String>(String s1, String s2) {
    public void accept(String s, String s2) {
        "hi"; // Here, the compiler will attempt to add a "return"
              // keyword before the "hi", but then it will fail
              // with "compiler error ... bla bla ...
              //  java.lang.String cannot be converted to void"
    }
});

В первом случае вы выполняете конструктор, но НЕ возвращаете созданный объект, во втором случае вы пытаетесь вернуть значение String, но ваш метод в вашем интерфейсе BiConsumerвозвращает void, отсюда и ошибка компилятора.

моргано
источник
12

JLS указывает, что

Если результат типа функции void, лямбда-тело является либо выражением инструкции (§14.8), либо блоком, совместимым с void.

Теперь давайте посмотрим на это подробнее,

Поскольку ваш takeBiConsumerметод имеет тип void, получение лямбда new String("hi")интерпретирует его как блок вроде

{
    new String("hi");
}

который действителен в пустоте, следовательно, первый случай компиляции.

Однако в случае, когда лямбда есть -> "hi", такой блок, как

{
    "hi";
}

недопустимый синтаксис в java. Поэтому единственное, что можно сделать со словом "привет" - это попытаться вернуть его.

{
    return "hi";
}

который недействителен в пустоте и объяснит сообщение об ошибке

incompatible types: bad return type in lambda expression
    java.lang.String cannot be converted to void

Для лучшего понимания обратите внимание, что если вы измените тип takeBiConsumerна String, -> "hi"он будет действителен, поскольку он просто попытается напрямую вернуть строку.


Обратите внимание, что сначала я подумал, что ошибка была вызвана тем, что лямбда находится в неправильном контексте вызова, поэтому я поделюсь этой возможностью с сообществом:

JLS 15.27

Это ошибка времени компиляции, если лямбда-выражение встречается в программе где-нибудь, кроме контекста присваивания (§5.2), контекста вызова (§5.3) или контекста приведения (§5.5).

Однако в нашем случае мы находимся в контексте вызова, что правильно.

Жан-Франсуа Савар
источник