Использование лямбда препятствует выводу переменной типа

У меня есть следующий код, который компилируется успешно:

import java.lang.String;
import java.util.List;
import java.util.Arrays;

interface Supplier<R> {
    Foo<R> get();
}

interface Foo<R> {
    public R getBar();
    public void init();  
}

public class Main {

    static private <V> void doSomething(final Supplier<? extends List<? extends V>> supplier) {
    // do something
    }

    static public void main(String[] args) {
        doSomething(new Supplier<List<Object>>(){
           @Override
           public Foo<List<Object>> get() {
               return new Foo<List<Object>>(){
                   @Override
                   public List<Object> getBar() {
                       return null;
                   }
                   @Override
                   public void init() {
                      // initialisation
                   }
               };
            }
       });
    }
}

Однако, если я конвертирую Supplier в следующее выражение лямбда, код больше не компилируется:

doSomething(() -> new Foo<List<Object>>(){
    @Override
    public List<Object> getBar() {
        return null;
    }
});

Ошибка компилятора:

Main.java:22: error: method doSomething in class Main cannot be applied to given types;
    doSomething(() -> new Foo<List<Object>>(){
    ^
  required: Supplier<? extends List<? extends V>>
  found: ()->new Fo[...]; } }
  reason: cannot infer type-variable(s) V
    (argument mismatch; bad return type in lambda expression
      <anonymous Foo<List<Object>>> cannot be converted to Foo<List<? extends V>>)
  where V is a type-variable:
    V extends Object declared in method <V>doSomething(Supplier<? extends List<? extends V>>)

Если я изменю объявление поставщика Supplier<? extends List<V>> Supplier<? extends List<V>>, оба варианта скомпилируются успешно.

Я компилирую код с помощью компилятора Java 8.

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

Ответы

Ответ 1

Если я использую:

doSomething(() -> () -> null);

Он просто отлично работает, и все типы правильно выведены компилятором.

Если я попытаюсь сделать так:

doSomething(() -> () -> 1);

Компиляция завершается неудачно, и это правильно, потому что метод doSomething ожидает Supplier<? extends List<? extends V>> Supplier<? extends List<? extends V>> Supplier<? extends List<? extends V>> аргумент Supplier<? extends List<? extends V>> и () ->() → 1 не является.

И если я это сделаю:

doSomething(() -> () -> Arrays.asList(1, 2, 3));

Он работает так, как ожидалось.

Таким образом, здесь нет необходимости ничего делать, просто используя lambdas и позволяя компилятору выполнять свою работу в порядке.


РЕДАКТИРОВАТЬ:

И если я это сделаю:

doSomething(() -> new Foo<List<? extends Object>>() {
    @Override
    public List<? extends Object> getBar() {
        return null;
    }
});

Он компилируется без ошибок.

Таким образом, проблема заключается в том, что компилятор считает, что List<Object> не совпадает с List<? extends Object> List<? extends Object>, и когда вы используете лямбда-выражения, он просто жалуется на это (ошибочно). Однако он не жалуется на анонимные внутренние классы, поэтому все это указывает на то, что это ошибка.

Ответ 2

В этой конкретной ситуации явный приведение может помочь:

doSomething((Supplier<List<Object>>) () -> new Foo<List<Object>>() {
                       ^
    @Override
    public List<Object> getBar() {
        return null;
    }
});

Или, еще проще:

doSomething((Supplier<List<Object>>) () -> (Foo<List<Object>>) () -> null);

doSomething(() -> () -> null);

Иногда компилятор Java не может вывести тип, соответствующий вашему намерению, поэтому вы можете указать его явно. Еще один пример без кастинга (посмотрите на левую часть объявления):

Supplier<List<Object>> supplier = () -> new Foo<List<Object>>() {
    @Override
    public List<Object> getBar() {
        return null;
    }
};

doSomething(supplier);

В то же время, когда вы пишете:

static <V> void doSomething(final Supplier<? extends List<? extends V>> supplier) {
}

doSomething(() -> new Foo<List<Object>>() {
    @Override
    public List<Object> getBar() {
        return null;
    }
});

ожидаемый тип возвращаемого значения в выражении лямбда:

Foo<List<? extends V>>

который не совпадает с фактическим:

Foo<List<Object>>

Компилятор сообщает вам об этом на выходе:

reason: cannot infer type-variable(s) V
    argument mismatch; bad return type in lambda expression
        <anonymous Foo<List<Object>>> cannot be converted to Foo<List<? extends V>>

Ответ 3

Проблема вызвана использованием ? extends V ? extends V со List в определении метода doSomething, но при вызове метода вы используете new Foo<List<Object>> напрямую.

Там, List<? extends Object> List<? extends Object> не равно List<Object>, Since ? extends Object ? extends Object - это ковариация с типом Object, например, вы не можете поместить элемент Object type в List<? extends Object> List<? extends Object>, потому что компилятор не может определить, какой тип должен быть ? extends Object ? extends Object.

Итак, для вашего примера вы можете попытаться исправить его, используя new Foo<List<? extends Object>>() new Foo<List<? extends Object>>(), может быть, например:

    doSomething(() -> new Foo<List<? extends Object>>() {
        @Override
        public List<Object> getBar() {
            return null;
        }
    });

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

class Main$1$1 implements Foo<java.util.List<java.lang.Object>> 
...
final class Main$1 implements SupplierT<java.util.List<java.lang.Object>> {
...

как вы можете видеть, это переопределяет ? extends V ? extends V как тип Object.

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

class Main$1 implements SupplierT<java.util.List<java.lang.Object>>

вышеупомянутый анонимный класс не будет генерировать, и lambda будет использовать invokedynamic инструкцию напрямую, как:

   0: invokedynamic #5,  0              // InvokeDynamic #0:get:()LSupplierT;

Так оно все еще пытается сделать вывод ? extends V ? extends V, это приведет к сбою компиляции.

Ссылка:

https://docs.oracle.com/javase/tutorial/java/generics/subtyping.html

или этот пример:

Является ли список подклассом List? Почему Java-дженерики не являются неявно полиморфными?