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

Следующий код 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

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

Ответ 1

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

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

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

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

  • Вызов конструктора выражение выражения, поэтому он компилируется.
  • Строковый литерал не является выражением оператора и не совместим с void (см. примеры из 15.27.2), поэтому он не компилируется.

Ответ 2

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

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

Ответ 3

Первый случай - это нормально, потому что вы вызываете "специальный" метод (конструктор), и вы фактически не принимаете созданный объект. Чтобы сделать это более ясным, я поставлю необязательные фигурные скобки в ваших лямбдах:

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, поэтому компилятор ошибка.

Ответ 4

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

Если результат типа функции недействителен, тело лямбда является либо выражение выражения (§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", поскольку он просто попытается напрямую вернуть строку.


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

JLS 15.27

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

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