Использование лямбда препятствует выводу переменной типа
У меня есть следующий код, который компилируется успешно:
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-дженерики не являются неявно полиморфными?