Java lambda возвращает лямбду

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

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;

public class so1 {
   public static void main() {
      List<Number> l = new ArrayList<>(Arrays.asList(1, 2, 3));
      List<Callable<Object>> checks = l.stream().
               map(n -> (Callable<Object>) () -> {
                  System.out.println(n); 
                  return null;
               }).
               collect(Collectors.toList());
   }
}

Он принимает список чисел и создает список функций, которые могут распечатывать их. Однако явное приведение к Callable кажется излишним. Мне кажется, и IntelliJ. И мы оба согласны с тем, что это также должно работать:

List<Callable<Object>> checks = l.stream().
       map(n -> () -> {
          System.out.println(n); 
          return null;
       }).
       collect(Collectors.toList());

Однако я получаю сообщение об ошибке:

so1.java:10: error: incompatible types: cannot infer type-variable(s) R
      List<Callable<Object>> checks = l.stream().map(n -> () -> {System.out.println(n); return null;}).collect(Collectors.toList());
                                                    ^
    (argument mismatch; bad return type in lambda expression
      Object is not a functional interface)
  where R,T are type-variables:
    R extends Object declared in method <R>map(Function<? super T,? extends R>)
    T extends Object declared in interface Stream
1 error

Ответ 1

Вы попадаете в ограничение набора символов Java 8s, которое применяется к получателю вызова метода. В то время как целевая типизация работает (в большинстве случаев) для типов параметров, она не работает для объекта или выражения, к которому вы вызываете метод.

Здесь l.stream(). map(n -> () -> { System.out.println(n); return null; }) является получателем вызова метода collect(Collectors.toList()), поэтому целевой тип List<Callable<Object>> не рассматривается для него.

Легко доказать, что вложенные лямбда-выражения работают, если известен тип цели, например

static <T> Function<T,Callable<Object>> toCallable() {
    return n -> () -> {
        System.out.println(n); 
        return null;
    };
}

работает без проблем, и вы можете использовать его для решения своей исходной проблемы как

List<Callable<Object>> checks = l.stream()
    .map(toCallable()).collect(Collectors.toList());

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

// turns the Stream s from receiver to a parameter
static <T, R, A> R collect(Stream<T> s, Collector<? super T, A, R> collector) {
    return s.collect(collector);
}

и перепишем исходное выражение как

List<Callable<Object>> checks = collect(l.stream().map(
    n -> () -> {
        System.out.println(n); 
        return null;
    }), Collectors.toList());

Это не уменьшает сложность кода, но может быть скомпилировано без каких-либо проблем. Для меня это дежавю. Когда вышли Java 5 и Generics, программистам пришлось повторять параметры типа в выражениях new, просто переносив выражение в общий метод, доказав, что вывод типа не представляет проблемы. Потребовалось до тех пор, пока Java 7 до того, как программистам не было позволено пропустить это ненужное повторение аргументов типа (используя "оператор бриллианта" ). Теперь мы имеем аналогичную ситуацию, обертывая выражение вызова в другой метод, превращая приемник в параметр, доказывает, что это ограничение не нужно. Поэтому, возможно, мы избавимся от этого ограничения в Java 10...

Ответ 2

Я еще не вникал в точные правила того, как работает вывод типа с lambdas. С точки зрения общего языка, однако, не всегда возможно писать языковые правила, которые позволяют компилятору разобраться во всем, что мы думаем. Я был компилятором для компилятора Ada-language, и я знаком со многими проблемами, связанными с языковым дизайном. Ada использует вывод типа во многих случаях (где тип конструкции не может быть определен без просмотра всего выражения, содержащего конструкцию, что, я думаю, также относится к этому выражению Java lambda). Существуют некоторые языковые правила, которые заставляют компиляторы отклонять некоторые выражения как неоднозначные, когда теоретически существует только одна возможная интерпретация. Одна из причин, если я правильно помню, заключается в том, что кто-то нашел случай, когда правило, которое позволило бы компилятору выяснить правильную интерпретацию, потребовало бы, чтобы компилятор сделал 17 проходов через выражение, чтобы правильно его интерпретировать.

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

Ответ 3

Я столкнулся с этой же проблемой и смог ее решить, явно указав общий тип-параметр на map следующим образом:

List<Callable<Object>> checks = l.stream().
   <Callable<Object>>map(n -> () -> {
      System.out.println(n); 
      return null;
   }).
   collect(Collectors.toList());