【问题标题】:Using lambda impedes inference of type variable使用 lambda 会阻碍类型变量的推断
【发布时间】:2019-02-25 22:50:40
【问题描述】:

我有以下代码编译成功:

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 转换为以下 lambda 表达式,则代码不再编译:

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&lt;? extends List&lt;V&gt;&gt;,则两个变体都编译成功。

我使用 Java 8 编译器编译代码。

为什么带有 lambda 的代码无法编译,虽然它等同于非 lambda 版本? 这是 Java 的已知/预期限制还是错误?

【问题讨论】:

  • 在 Java 10 中,它似乎使用菱形运算符 () -&gt; new Foo&lt;&gt;() {...} 进行编译(我不确定它是否在 Java 8 上编译,或者它是否符合您的要求)。
  • 在 Java 8 中,它不使用菱形运算符进行编译。编译器给了我以下错误:cannot infer type arguments for Foo&lt;R&gt;

标签: java generics lambda java-8


【解决方案1】:

如果我使用:

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

它工作正常,编译器正确推断出所有类型。

如果我尝试你做:

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

编译失败,这是正确的,因为 doSomething 方法需要一个 Supplier&lt;? extends List&lt;? extends V&gt;&gt; 参数,而 () -&gt; () -&gt; 1 不是。

如果我这样做:

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

它按预期工作。

所以这里不需要强制转换任何东西,只需使用 lambdas 并让编译器完成它的工作就可以了。


编辑:

如果我这样做:

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

它编译没有错误。

所以最重要的是,问题在于编译器认为 List&lt;Object&gt;List&lt;? extends Object&gt; 不同,当您使用 lambda 表达式时,它只是抱怨它(错)。不过,它不会抱怨匿名内部类,所以这一切都表明这是一个错误。

【讨论】:

  • 显然,我选择的最小示例太小了。在受此问题影响的实际代码中,Foo 接口的方法不止一种。因此,您不能使用第二个 lambda。我很抱歉这个误导性的例子。我调整了最小示例以更好地匹配它来自的代码。无论如何,这种行为更加奇怪。编译器用两个连续的 lambdas 推断类型变量没有问题,但用一个 lambdas 失败。
  • @Federico 当然,这些东西是“相同的”,而不是同时。一个只是生产者,所以编译器可能会以某种方式围绕它做一些事情......不过,看起来像一个错误,因为它使用&lt;&gt; 在 java-10 及更高版本中编译
  • @Eugene Here's the article。他们在那里解释说,Java 9 对 diamond 进行了改进,以支持匿名内部类的实例化。所以我认为钻石改进与此无关。这只是 Java 8 中关于使用 lambda 进行类型推断的一个错误。
  • @FedericoPeraltaSchaffner 现在只要我们能找到相关的错误......
  • @Bruno 我会说这只是一个错误。
【解决方案2】:

问题是在doSomething方法定义中使用? extends VList引起的,但是在调用方法时,您是直接使用new Foo&lt;List&lt;Object&gt;&gt;

其中List&lt;? extends Object&gt;不等于List&lt;Object&gt;,因为? extends ObjectObject类型是协变的,比如你不能把Object类型元素放入List&lt;? extends Object&gt;,因为编译器无法推断? extends Object应该是什么类型。

因此,对于您的示例,您可以尝试直接使用new Foo&lt;List&lt;? extends Object&gt;&gt;() 来修复它,可能像:

    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 覆盖为Object 类型。

但是对于lambda,它不会生成对应的匿名类,例如:

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

上面的匿名类不会生成,lambda会使用invokedynamic指令直接调用,如:

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

所以还是尝试推断? extends V,会导致编译失败

参考:

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

或者这个例子:

Is List a subclass of List? Why are Java generics not implicitly polymorphic?

【讨论】:

  • 但是为什么没有 lambda 也能工作呢?如果这是一个不兼容的泛型问题,它也不应该在那里工作,对吧?
  • @Bruno,很好的调用,关键是:lambda 不会生成匿名类,它不能推断类型。在更新中查看更多内容,希望清楚
  • doSomething(() -&gt; new Foo&lt;List&lt;?&gt;&gt;() { ... }); 也可以正常工作。
  • @Oleksandr,我认为这是因为? 通配符明确扩展了Object,所以如果将? extends Object 更改为? extends BigInteger 等其他类型,它应该会失败。
  • @chengpohi 读了这个答案两次,我怀疑它回答了这个问题,而且充其量是误导
【解决方案3】:

在这种特殊情况下,显式转换可以提供帮助:

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 编译器无法推断出符合您意图的类型,因此您可以显式指定它。还有一个没有强制转换的例子(看看声明的left-hand):

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;
    }
});

lambda 表达式中的预期返回类型是:

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>>

【讨论】:

  • 您的解决方案有效。但是,我不明白为什么这里需要显式转换。编译器具有推断类型变量的所有信息。此外,了解在哪些情况下需要显式转换会很有趣。还是只是反复试验?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多