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

Java 8 Потребительская/Функция Lambda Ambiguity

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

Основываясь на моем чтении JLS §15.12.2.1. Определите потенциально применимые методы: кажется, что компилятор должен знать, что моя лямбда с блоком void соответствует методу 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);
    }
  }
}

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

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

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

Ответ 1

Эта строка однозначно неоднозначна:

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

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

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

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

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

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

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

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, но он не квалифицируется как пустота - совместимый блок. Однако у вас могут быть проблемы с блоками:

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

Этот блок квалифицируется как совместимый по стоимости и совместимый с void, так что у вас снова есть неоднозначность. Еще один пример блокировки из-за границы - бесконечный цикл:

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

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

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

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

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

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