Лямбда-выражение и метод перегружают сомнения

Итак, перегрузка методов — это плохо™. Теперь, когда это решено, давайте предположим, что я действительно хочу перегрузить такой метод:

static void run(Consumer<Integer> consumer) {
    System.out.println("consumer");
}

static void run(Function<Integer, Integer> function) {
    System.out.println("function");
}

В Java 7 я мог бы легко вызывать их с недвусмысленными анонимными классами в качестве аргументов:

run(new Consumer<Integer>() {
    public void accept(Integer integer) {}
});

run(new Function<Integer, Integer>() {
    public Integer apply(Integer o) { return 1; }
});

Теперь, в Java 8, я, конечно, хотел бы вызывать эти методы с помощью лямбда-выражений, и я могу это сделать!

// Consumer
run((Integer i) -> {});

// Function
run((Integer i) -> 1);

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

// Consumer
run(i -> {});

// Function
run(i -> 1);

Но это не компилируется. Компилятору (javac, jdk1.8.0_05) это не нравится:

Test.java:63: error: reference to run is ambiguous
        run(i -> {});
        ^
  both method run(Consumer<Integer>) in Test and 
       method run(Function<Integer,Integer>) in Test match

Для меня интуитивно это не имеет смысла. Между лямбда-выражением, которое возвращает возвращаемое значение (совместимо со значением), и лямбда-выражением, которое дает void (совместимо с void), как указано в документе JLS §15.27.

Но, конечно же, JLS глубок и сложен, и мы унаследовали 20-летнюю историю обратной совместимости, и есть новые вещи, такие как:

Некоторые выражения аргументов, содержащие лямбда-выражения с неявным типом (§15.27.1) или неточные ссылки на методы (§15.13.1) игнорируются тестами применимости, поскольку их значение не может быть определено до тех пор, пока не будет задан целевой тип. выбран.

из JLS §15.12 .2

Указанное выше ограничение, вероятно, связано с тем, что JEP 101 не был полностью реализован, т.к. можно увидеть здесь и здесь.

Вопрос:

Кто может мне точно сказать, какие части JLS указывают на эту двусмысленность во время компиляции (или это ошибка компилятора)?

Бонус: почему все решилось именно так?

Обновлять:

С jdk1.8.0_40 вышеприведенное компилируется и работает нормально


person Lukas Eder    schedule 02.05.2014    source источник
comment
Просто для уточнения. Когда вы говорите бег (i -> {}); что «i» может быть Consumer‹Integer› или Function‹Integer, Integer›. Эта двусмысленность очевидна, верно?   -  person Syam S    schedule 02.05.2014
comment
@SyamS: i — это первый (и единственный) аргумент либо Consumer.accept(), либо Function.apply(). Это само по себе может быть двусмысленным. Но учитывая, что одна лямбда оценивается как тип, совместимый со значением (Function), а другая оценивается как тип, совместимый с void (Consumer), я интуитивно думаю, что двусмысленности нет.   -  person Lukas Eder    schedule 02.05.2014
comment
Извините, если это звучит глупо, но перегрузка функций обычно зависит только от типа ввода. Он не проверяет возвращаемые типы. Итак, в этом случае accept и apply оба принимают один аргумент типа Integer. Так что мне это кажется двусмысленным. :) Ищет ли лямбда возвращаемый тип для вывода?   -  person Syam S    schedule 02.05.2014
comment
Он работал с более ранними версиями (например, beta 102 и ранее).   -  person Holger    schedule 02.05.2014
comment
@SyamS Это кажется двусмысленностью, но компилятор может понять, что run((Integer i) -> {}) является Потребителем. Таким образом, несмотря на то, что это может быть как Function‹Integer,Integer›, так и Consumer‹Integer›, Consumer‹Integer› лучше всего подходит, и компилятор использует его. Вопрос в том, почему компилятор делает это только тогда, когда вы указываете (Integer i), а не только i.   -  person jacobhyphenated    schedule 02.05.2014
comment
@SyamS: i -> {} никогда не может оцениваться как Function, потому что он совместим с void. i -> 1 никогда не может быть оценено как Consumer, потому что оно совместимо по значению. На мой взгляд, для каждого вызова даже применим только один из перегруженных методов. Как также указал @jacobhyphenated, двусмысленность можно устранить, явно указав идентичные типы аргументов функции (Integer i).   -  person Lukas Eder    schedule 02.05.2014
comment
@LukasEder Вы правы. Если я попытаюсь указать run((Consumer<Integer>) (Integer i) -> {1});, он не скомпилируется. Это наводит меня на мысль, что это должна быть ошибка компилятора, поскольку между двумя лямбда-выражениями действительно нет двусмысленности.   -  person jacobhyphenated    schedule 02.05.2014
comment
@jacobhyphenated: Я бы хотел этого. Я очень хочу перегрузить два таких метода в API jOOQ, не создавая проблем для пользователей Java 8 :-) Но я думаю, что ваш ход мыслей еще не является формальным доказательством того, что это ошибка. Я подозреваю, что это действительно ограничение JLS.   -  person Lukas Eder    schedule 02.05.2014
comment
Спасибо вам обоим. Я только начал изучать Java 8. Когда-то я видел видео Брайана Гетца, Lambda: заглянуть под капот. Он говорил обо всех этих дизайнерских соображениях. Я не смотрел его полностью, так как не мог понять большую часть этого. Может быть, это поможет вам. Видео можно найти на странице youtube.com/watch?v=9JRDbjQRhRw.   -  person Syam S    schedule 02.05.2014
comment
@SyamS: Я несколько сомневаюсь, что Брайан Гетц утомил бы общую аудиторию JAX такими языковыми подробностями, которые могли обсуждаться где-то в глубинах списка рассылки lambda-dev :-)   -  person Lukas Eder    schedule 02.05.2014
comment
Правая часть лямбда-выражения не может быть определена без информации о типе. i->i.thing() может быть void или значением, мы не знаем, пока не знаем, что такое i. Кажется, что компилятор не хочет рассуждать об этом, несмотря на то, что обе лямбды имеют Integer параметра.   -  person ggovan    schedule 02.05.2014
comment
@ggovan: Я думаю, что это может быть правильным ключом. Вероятно, меня смущает тот факт, что мои конкретные лямбда-выражения однозначны (i -> 1 и i -> {}), тогда как лямбда-выражения вообще могут быть неоднозначными (i -> intFunction() и i -> voidFunction()). Теперь докажи это, и я приму твой ответ :-)   -  person Lukas Eder    schedule 02.05.2014


Ответы (3)


Я думаю, вы нашли эту ошибку в компиляторе: JDK-8029718 (или аналогичный в Eclipse: 434642).

Сравните с JLS §15.12.2.1. . Определите потенциально применимые методы:

  • Лямбда-выражение (§15.27) потенциально совместимо с типом функционального интерфейса (§9.8), если выполняются все следующие условия:

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

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

    • Если тип функции целевого типа имеет возвращаемый тип (не пустой), то тело лямбда-выражения является либо выражением, либо блоком, совместимым со значением (§15.27.2).

Обратите внимание на четкое различие между «void совместимыми блоками» и «блоками, совместимыми по значению». Хотя в некоторых случаях блок может быть и тем и другим, раздел §15.27.2. В Lambda Body четко указано, что такое выражение, как () -> {}, является «void совместимым блоком», так как обычно завершается без возврата значения. И должно быть очевидно, что i -> {} также является «void совместимым блоком».

И, согласно приведенному выше разделу, комбинация лямбда-выражения с блоком, несовместимым по значению, и целевой тип с возвращаемым типом (не-void) не является потенциальным кандидатом на разрешение перегрузки метода. Так что ваша интуиция верна, здесь не должно быть никакой двусмысленности.

Примеры неоднозначных блоков:

() -> { throw new RuntimeException(); }
() -> { while (true); }

так как они нормально не завершаются, но в вашем вопросе это не так.

person Holger    schedule 02.05.2014
comment
Для лямбда i->i.thing() вы не можете сказать, является ли это значением void или value, не зная типа i. Может ли это быть проблемой, а не ошибкой компилятора. - person ggovan; 02.05.2014
comment
Спасибо. Я тоже был там, но потом увидел JLS §15.12.2.5, который касается только явно типизированных лямбда-выражений, но не неявно типизированных лямбда-выражений. Похоже, что мой код проходит через все §15.12.2.5, чтобы достичь . В противном случае вызов метода неоднозначен и возникает ошибка времени компиляции. - person Lukas Eder; 02.05.2014
comment
@ggovan: мы говорим о i->{}, который явно относится к типу void, и i->1, который явно относится к типу значения. Как указано в вопросе… - person Holger; 02.05.2014
comment
@ Лукас Эдер: §15.12.2.5 посвящен «Выбору наиболее конкретного метода», который применяется, когда применимо более одного метода, но из-за «void против типа значения» применим только один метод, поэтому этот раздел не применяется к нашему ситуации (ну, это должно быть здесь неуместным…). - person Holger; 02.05.2014
comment
@Holger: я знаю, это не должно применяться. Но проверьте еще раз мой раздел в кавычках: Некоторые выражения аргументов, которые содержат неявно типизированные лямбда-выражения (§15.27.1) или неточные ссылки на методы (§15.13.1), игнорируются тестами применимости, потому что их значение не может быть определено до тех пор, пока выбран целевой тип. Возможно, раздел применимости просто пропущен для неявно типизированных лямбда-выражений - person Lukas Eder; 03.05.2014
comment
Это правильный ответ. Ошибка была исправлена ​​- bugs.openjdk.java.net/browse/JDK-8029718< /а> - person ZhongYu; 04.05.2014
comment
@LukasEder лямбда-выражение используется в 15.12.2.1, так что мы получили только один потенциально применимый метод (); в (15.12.2.2) он игнорируется, и метод оказывается применимым. поскольку это единственный применимый метод, (15.12.2.5) не применяется. - person ZhongYu; 04.05.2014
comment
@zhong.j.yu: Звучит очень разумно, спасибо. Я еще раз проверю это на ранней сборке 8u20. Почему вы удалили свой ответ? - person Lukas Eder; 04.05.2014
comment
@LukasEder Я думаю, что ответ Хольгера идеален. - person ZhongYu; 04.05.2014
comment
@LukasEder нет, я думаю, что здесь применима его точка (1), в которой он согласен с тем, что это была ошибка. - person ZhongYu; 04.05.2014
comment
@zhong.j.yu: Но он явно ссылается на 1) Пример в описании и 2) Пример функции и потребителя не является ошибкой. Последнее в значительной степени соответствует моему собственному примеру, я думаю? - person Lukas Eder; 04.05.2014
comment
@LukasEder в примере (2) тело лямбда представляет собой выражение оператора, но в вашем вопросе тело лямбда представляет собой блок. ошибка связана с телом блока; см. исходный отчет, вызвавший ошибку — mail.openjdk .java.net/pipermail/lambda-dev/2013-ноябрь/ - person ZhongYu; 04.05.2014
comment
Но оба вызова моего примера не компилируются, независимо от того, есть ли у нас лямбда, совместимая с void или с выражением... И я бы сказал, что пример (2) также является лямбдой, совместимой с void, нет? System.gc() возвращает void - person Lukas Eder; 04.05.2014
comment
@LukasEder в примере (2), не зная типа параметра лямбда s, javac не будет компилировать тело лямбда, поэтому тип возвращаемого значения в этот момент неизвестен. Если лямбда явная - foo( (String s)->System.gc() ), все будет хорошо. но можно возразить, что s в любом случае, очевидно, является String... отсюда и дискуссия, о которой я упоминал в другом комментарии. - person ZhongYu; 04.05.2014
comment
@LukasEder в вашем примере тело лямбда представляет собой блок, javac, по крайней мере, знает, возвращает ли оно пустоту, просто по структуре кода. - person ZhongYu; 04.05.2014
comment
@Holger: я добавил ссылку на ошибку в ваш ответ, поскольку гипотеза Чжуна подтверждено на lambda-dev - person Lukas Eder; 07.05.2014
comment
Просто чтобы закрыть петлю: в Eclipse эта ошибка была преднамеренно вставлена ​​только для того, чтобы имитировать поведение javac. Это было отменено вскоре после исправления ошибки javac (см. ссылки в ответе). - person Stephan Herrmann; 23.07.2015

Об этой ошибке уже сообщалось в системе ошибок JDK: https://bugs.openjdk.java.net/browse/JDK-8029718. Как вы можете убедиться, ошибка была исправлена. Это исправление синхронизирует javac со спецификацией в этом аспекте. Прямо сейчас javac правильно принимает версию с неявными лямбда-выражениями. Чтобы получить это обновление, необходимо клонировать репозиторий javac 8.

Что делает исправление, так это анализирует тело лямбда и определяет, совместимо ли оно с пустотой или значением. Чтобы определить это, вам необходимо проанализировать все операторы возврата. Давайте вспомним, что из спецификации (15.27.2), уже упомянутой выше:

  • Тело лямбда-выражения блока совместимо с void, если каждый оператор return в блоке имеет форму return.
  • Тело лямбда-выражения блока совместимо по значению, если оно не может завершиться нормально (14.21), и каждый оператор return в блоке имеет форму return Expression.

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

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

class Test {
    interface I {
        String f(String x);
    }

    static void foo(I i) {}

    void m() {
        foo((x) -> {
            if (x == null) {
                return;
            } else {
                return x;
            }
        });
    }
}

компилятор выдаст такой вывод:

Test.java:9: error: lambda body is neither value nor void compatible
    foo((x) -> {
        ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

Надеюсь, это поможет.

person Vicente Romero    schedule 06.05.2014

Предположим, у нас есть метод и вызов метода

void run(Function<Integer, Integer> f)

run(i->i)

Какие методы мы можем легально добавить?

void run(BiFunction<Integer, Integer, Integer> f)
void run(Supplier<Integer> f)

Здесь арность параметра другая, в частности, i-> часть i->i не соответствует параметрам apply(T,U) в BiFunction или get() в Supplier. Так что здесь любые возможные неоднозначности определяются арностью параметра, а не типами и не возвратом.


Какие методы мы не можем добавить?

void run(Function<Integer, String> f)

Это дает ошибку компилятора как run(..) and run(..) have the same erasure. Так как JVM не может поддерживать две функции с одинаковыми именами и типами аргументов, это невозможно скомпилировать. Таким образом, компилятору никогда не приходится разрешать неоднозначности в этом типе сценария, поскольку они явно запрещены из-за правил, ранее существовавших в системе типов Java.

Таким образом, у нас остаются другие функциональные типы с арностью параметра, равной 1.

void run(IntUnaryOperator f)

Здесь run(i->i) допустимо как для Function, так и для IntUnaryOperator, но это не будет компилироваться из-за reference to run is ambiguous, так как обе функции соответствуют этой лямбде. На самом деле они это делают, и здесь следует ожидать ошибки.

interface X { void thing();}
interface Y { String thing();}

void run(Function<Y,String> f)
void run(Consumer<X> f)
run(i->i.thing())

Здесь это не скомпилируется, опять же из-за двусмысленности. Не зная типа i в этой лямбде, невозможно узнать тип i.thing(). Поэтому мы признаем, что это неоднозначно и правильно не компилируется.


В вашем примере:

void run(Consumer<Integer> f)
void run(Function<Integer,Integer> f)
run(i->i)

Здесь мы знаем, что оба функциональных типа имеют один параметр Integer, поэтому мы знаем, что i в i-> должно быть Integer. Итак, мы знаем, что должно быть вызвано run(Function). Но компилятор не пытается это сделать. Это первый раз, когда компилятор делает что-то, чего мы не ожидаем.

Почему это не делается? Я бы сказал, потому что это очень специфический случай, и вывод типа здесь требует механизмов, которых мы не видели ни в одном из других вышеперечисленных случаев, потому что в общем случае они не могут правильно определить тип и выбрать правильный метод. .

person ggovan    schedule 02.05.2014
comment
были жаркие споры о том, должен ли run(i->1) компилироваться. Здесь нет двусмысленности или сложности, поскольку i, очевидно, является целым числом. к сожалению, они решили не поддерживать его в настоящее время, но оставили дверь открытой для рассмотрения в будущем (если эта функция понадобится достаточному количеству людей) - person ZhongYu; 04.05.2014
comment
@zhong.j.yu: я подозреваю, что эта дискуссия также является тот, который вы здесь цитировали? - person Lukas Eder; 04.05.2014
comment
@LukasEder, это было долгое и запутанное обсуждение, начавшееся с mail.openjdk.java.net/pipermail/lambda-spec-observers/ — не рекомендую читать :) никто не знал, о чем друг с другом говорили. - person ZhongYu; 04.05.2014