Подтвердить что ты не робот

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

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

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-compatible" ), как указано в JLS §15.27.

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

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

из JLS §15.12.2

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

Вопрос:

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

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

Обновление:

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

4b9b3361

Ответ 1

Я думаю, что вы нашли эту ошибку в компиляторе: 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); }

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

Ответ 2

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

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

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

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

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

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

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

Ответ 3

Предположим, что мы вызываем метод и метод

void run(Function<Integer, Integer> f)

run(i->i)

Какие методы мы можем юридически добавить?

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

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


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

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), которое называется. Но компилятор не пытается это сделать. Это первый случай, когда компилятор делает то, чего мы не ожидаем.

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