Неоднозначность потребителя/функции Java 8 Lambda

У меня есть перегруженный метод, который принимает объект Consumer и Function соответственно и возвращает общий тип, соответствующий соответствующему Consumer/Function. Я думал, что все будет хорошо, но когда я пытаюсь вызвать любой метод с лямбда-выражением, я получаю сообщение об ошибке, указывающее, что ссылка на метод неоднозначна.

Основываясь на моем прочтении JLS § 15.12.2.1. Определите потенциально применимые методы: кажется, что компилятор должен знать, что моя лямбда с пустым блоком соответствует методу Consumer, а моя лямбда с возвращаемым типом соответствует методу Function.

Я собрал следующий пример кода, который не компилируется:

import java.util.function.Consumer;
import java.util.function.Function;

public class AmbiguityBug {
  public static void main(String[] args) {
    doStuff(getPattern(x -> System.out.println(x)));
    doStuff(getPattern(x -> String.valueOf(x)));
  }

  static Pattern<String, String> getPattern(Function<String, String> function) {
    return new Pattern<>(function);
  }

  static ConsumablePattern<String> getPattern(Consumer<String> consumer) {
    return new ConsumablePattern<>(consumer);
  }

  static void doStuff(Pattern<String, String> pattern) {
    String result = pattern.apply("Hello World");
    System.out.println(result);
  }

  static void doStuff(ConsumablePattern<String> consumablePattern) {
    consumablePattern.consume("Hello World");
  }

  public static class Pattern<T, R> {
    private final Function<T, R> function;

    public Pattern(Function<T, R> function) {
      this.function = function;
    }

    public R apply(T value) {
      return function.apply(value);
    }
  }

  public static class ConsumablePattern<T> {
    private final Consumer<T> consumer;

    public ConsumablePattern(Consumer<T> consumer) {
      this.consumer = consumer;
    }

    public void consume(T value) {
      consumer.accept(value);
    }
  }
}

Я также нашел похожее сообщение о stackoverflow, которое оказалось ошибкой компилятора. Мой случай очень похож, хотя немного сложнее. Для меня это все еще выглядит как ошибка, но я хотел убедиться, что не ошибаюсь в спецификации языка для лямбда-выражений. Я использую Java 8u45, в которой должны быть все последние исправления.

Если я изменю вызовы методов, чтобы они были обернуты в блок, кажется, что все компилируется, но это добавляет дополнительную многословность, и многие автоформатеры переформатируют его в несколько строк.

doStuff(getPattern(x -> { System.out.println(x); }));
doStuff(getPattern(x -> { return String.valueOf(x); }));

person johnlcox    schedule 01.06.2015    source источник
comment
В вызовах getPattern, которые (должны) использовать Consumer, отсутствуют правильные скобки (я не могу редактировать это из-за минимального ограничения символов).   -  person    schedule 02.06.2015
comment
Другой обходной путь — указать тип x: doStuff(getPattern((String x) -> System.out.println(x))); doStuff(getPattern((String x) -> String.valueOf(x)));   -  person Misha    schedule 02.06.2015
comment
@Misha: мне это действительно кажется странным. У меня нет разумного объяснения, почему добавление типа x меняет ситуацию. Ожидаете ли вы, что этот случай будет отображаться на Function: Map<String, String> map = new HashMap<>();getPattern((String x) -> map.remove(x));?   -  person Tagir Valeev    schedule 02.06.2015
comment
@TagirValeev См. документы .oracle.com/javase/specs/jls/se8/html/. Существуют специальные правила для определения более конкретных отношений между типами функциональных интерфейсов, если лямбда типизирована явно. В соответствии с этими правилами Function кажется более конкретным, чем Consumer.   -  person Misha    schedule 02.06.2015
comment
Вы можете использовать x -> (String.valueOf(x)) для вызова метода на основе Function и x->{String.valueOf(x);} для вызова метода на основе Consumer. Но учитывая тот факт, что эти методы делают совершенно разные вещи и выдают результаты разного типа, что приводит к еще одному перегруженному выбору методов, которые снова делают совершенно разные вещи, использование одинаковых имен для этих методов является ужасной маскировкой.   -  person Holger    schedule 02.06.2015


Ответы (1)


Эта строка определенно двусмысленна:

doStuff(getPattern(x -> String.valueOf(x)));

Перечитайте это из связанной главы JLS:

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

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

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

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

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

public void test(Object x) {
    String.valueOf(x);
}

Это не имеет смысла, но прекрасно компилируется. Ваш метод может иметь побочный эффект, компилятор об этом не знает. Например, если бы это было List.add, которое всегда возвращает true и никто не заботится о его возвращаемом значении.

Конечно, эта лямбда также подходит для Function, поскольку это выражение. Таким образом, это неоднозначно. Если у вас есть что-то, что является выражением, но не выражением оператора, то вызов будет без проблем сопоставлен с Function:

doStuff(getPattern(x -> x == null ? "" : String.valueOf(x)));

Когда вы меняете его на { return String.valueOf(x); }, вы создаете совместимый по значению блок, поэтому он соответствует Function, но не квалифицируется как блок, совместимый с void. Однако у вас могут возникнуть проблемы и с блоками:

doStuff(getPattern(x -> {throw new UnsupportedOperationException();}));

Этот блок квалифицируется и как совместимый по значению, и как совместимый с пустотой, поэтому у вас снова возникает двусмысленность. Другой пример неоднозначного блока — бесконечный цикл:

doStuff(getPattern(x -> {while(true) System.out.println(x);}));

Что касается случая System.out.println(x), тут немного сложно. Это, безусловно, квалифицируется как выражение оператора, поэтому может быть сопоставлено с Consumer, но кажется, что оно соответствует выражению, а спецификация говорит, что вызов метода — это выражение. Однако это выражение ограниченного использования как 15.12.3 говорит:

Если объявление времени компиляции недействительно, то вызов метода должен быть выражением верхнего уровня (то есть Expression в операторе выражения или в части ForInit или ForUpdate оператора for), иначе возникает ошибка времени компиляции. Вызов такого метода не дает значения, поэтому его следует использовать только в ситуации, когда значение не требуется.

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

Таким образом, для меня оба утверждения компилируются в соответствии со спецификацией. Компилятор ECJ выдает те же сообщения об ошибках в этом коде.

В общем, я бы посоветовал вам избегать перегрузки ваших методов, когда ваши перегрузки имеют одинаковое количество параметров и имеют разницу только в принятом функциональном интерфейсе. Даже если эти функциональные интерфейсы имеют разную арность (например, Consumer и BiConsumer): у вас не будет проблем с лямбдой, но могут возникнуть проблемы со ссылками на методы. Просто выберите в этом случае разные имена для своих методов (например, processStuff и consumeStuff).

person Tagir Valeev    schedule 02.06.2015
comment
Почему между Consumer<A> и Function<A, B> возникает такая неоднозначность, а между Runner и Supplier<B> нет? - person Sasha; 20.10.2017
comment
т.е. cf(i -> Math.abs(i)) (где cf определено как для Consumer<A>, так и для Function<A, B>) вызывает неоднозначность, а rs(() -> Math.abs(0)) (где rs определено как для Runner, так и для Supplier<B>) нет. - person Sasha; 20.10.2017
comment
@Sasha, потому что арность типа функции целевого типа такая же, как арность лямбда-выражения. В вашем случае это тип A. Тип возврата B выходит за рамки анализа. - person Woland; 20.03.2019
comment
@Woland, разве Supplier<B> и Runnable не имеют одинаковую арность? - person Sasha; 20.03.2019
comment
@Sasha Поставщик **T** get(), работающий **void** run(). Таким образом, тип возвращаемого значения влияет на перегрузку в случае @FunctionalInterface. - person Woland; 21.03.2019
comment
@Woland, Функция **R** apply(T t), Потребитель **void** accept(T t) — так? - person Sasha; 21.03.2019