使用泛型和Lambda重载方法时模棱两可的方法调用 [英] Ambiguous method call when overloading method with generics and lambdas

查看:96
本文介绍了使用泛型和Lambda重载方法时模棱两可的方法调用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我注意到使用泛型和lambda重载方法的行为很怪异.此类可以正常工作:

  public <T> void test(T t) { }

  public <T> void test(Supplier<T> t) { }

  public void test() {
    test("test");
    test(() -> "test");
  }

没有模棱两可的方法调用.但是,将其更改为此将使第二个调用变得模棱两可:

  public <T> void test(Class<T> c, T t) { }

  public <T> void test(Class<T> c, Supplier<T> t) { }

  public void test() {
    test(String.class, "test");
    test(String.class, () -> "test"); // this line does not compile
  }

怎么可能?为什么添加另一个参数会导致方法解析不明确?为什么在第一个示例中可以分辨出Supplier和Object之间的区别,而在第二个示例中却不能呢?

这是使用1.8.0_121.这是完整的错误消息:

error: reference to test is ambiguous
    test(String.class, () -> "test");
    ^
  both method <T#1>test(Class<T#1>,T#1) in TestFoo and method <T#2>test(Class<T#2>,Supplier<T#2>) in TestFoo match
  where T#1,T#2 are type-variables:
    T#1 extends Object declared in method <T#1>test(Class<T#1>,T#1)
    T#2 extends Object declared in method <T#2>test(Class<T#2>,Supplier<T#2>)
/workspace/com/test/TestFoo.java:14: error: incompatible types: cannot infer type-variable(s) T
    test(String.class, () -> "test");
        ^
    (argument mismatch; String is not a functional interface)
  where T is a type-variable:
    T extends Object declared in method <T>test(Class<T>,T)

解决方案

如果我对Java SE JLS的JLS第15和18章的理解是正确的,那么问题的关键在于以下 15.12.2 :

适用性测试将忽略包含隐式类型的lambda表达式(第15.27.1节)或不精确的方法引用(第15.13.1节)的某些参数表达式,因为在选择目标类型之前,无法确定其含义.

当Java编译器遇到诸如test(() -> "test")之类的方法调用表达式时,它必须搜索可将此方法调用分派到的可访问(可见)和适用(即具有匹配签名)的方法.在您的第一个示例中,<T> void test(T)<T> void test(Supplier<T>)都可以访问并且适用. test(() -> "test")方法调用.在这种情况下,当存在多个匹配方法时,编译器将尝试确定最具体的方法.现在,虽然确定通用方法(如 JLS 15.12.2.5 JLS 18.5. 4 )非常复杂,我们可以使用从15.12.2.5开头的直觉:

非正式的直觉是,如果第一个方法处理的任何调用都可以传递给另一个方法而不会产生编译时错误,则一个方法比另一个方法更具体.

由于对<T> void test(Supplier<T>)的任何有效调用,我们都可以在<T> void test(T)中找到类型参数T的对应实例,因此前者比后者更具体.

现在,令人惊讶的部分是,在第二个示例中,即使我们很清楚,<T> void test(Class<T>, Supplier<T>)<T> void test(Class<T>, T)都被认为适用于方法调用test(String.class, () -> "test") .不应该.问题是,如上所述,在存在隐式类型的lambda的情况下,编译器的行为非常保守.特别请参见 JLS 18.5. 1 :

一组约束公式C的构造如下.

...

  • 通过严格调用来测试适用性:

如果k≠n,或者存在i(1≤i≤n)使得e_i与适用性有关(§15.12.2.2)(...)否则,对于所有与e_i有关的i(1≤i≤k),C包括‹e_i→F_iθ›.

  • 通过宽松调用来测试适用性:

如果k≠n,则该方法不适用,无需进行推断.

否则,对于所有与e_i有关的i(1≤i≤k),C包括‹e_i→F_iθ›.

JLS 15.12 .2.2 :

对于可能适用的方法m,自变量表达式被认为与适用性相关,除非它具有以下形式之一:

  • 隐式类型的lambda表达式(第15.27.1节).

...

因此,在方法适用性检查的上下文中,作为参数传递的隐式类型的lambda的约束不参与解决类型推断.

现在,如果我们假定这两种方法均适用,那么问题以及本例与前面的示例之间的区别是,这些方法都没有一个更具体.存在对<T> void test(Class<T>, Supplier<T>)有效但对<T> void test(Class<T>, T)无效的调用,反之亦然.

这也解释了为什么test(String.class, (Supplier<String>) () -> "test");进行编译,正如@Aominè在上面的评论中提到的那样. (Supplier<String>) () -> "test")是显式类型的lambda,因此被认为与适用性有关 ,编译器能够正确推断出这些方法中只有一种适用,并且不会发生冲突.

I've noticed a weird behavior for overloading methods with generics and lambdas. This class works fine:

  public <T> void test(T t) { }

  public <T> void test(Supplier<T> t) { }

  public void test() {
    test("test");
    test(() -> "test");
  }

No ambiguous method call. However, changing it to this makes the second call ambiguous:

  public <T> void test(Class<T> c, T t) { }

  public <T> void test(Class<T> c, Supplier<T> t) { }

  public void test() {
    test(String.class, "test");
    test(String.class, () -> "test"); // this line does not compile
  }

How can this be? Why would adding another argument cause the method resolution to be ambiguous? Why can it tell the difference between a Supplier and an Object in the first example, but not the second?

Edit: This is using 1.8.0_121. This is the full error message:

error: reference to test is ambiguous
    test(String.class, () -> "test");
    ^
  both method <T#1>test(Class<T#1>,T#1) in TestFoo and method <T#2>test(Class<T#2>,Supplier<T#2>) in TestFoo match
  where T#1,T#2 are type-variables:
    T#1 extends Object declared in method <T#1>test(Class<T#1>,T#1)
    T#2 extends Object declared in method <T#2>test(Class<T#2>,Supplier<T#2>)
/workspace/com/test/TestFoo.java:14: error: incompatible types: cannot infer type-variable(s) T
    test(String.class, () -> "test");
        ^
    (argument mismatch; String is not a functional interface)
  where T is a type-variable:
    T extends Object declared in method <T>test(Class<T>,T)

解决方案

If my understanding of chapters 15 and 18 of the JLS for Java SE 8 is correct, the key to your question lies in the following quote from paragraph 15.12.2:

Certain argument expressions that contain implicitly typed lambda expressions (§15.27.1) or inexact method references (§15.13.1) are ignored by the applicability tests, because their meaning cannot be determined until a target type is selected.

When a Java compiler encounters a method call expression such as test(() -> "test"), it has to search for accessible (visible) and applicable (i.e. with matching signature) methods to which this method call can be dispatched. In your first example, both <T> void test(T) and <T> void test(Supplier<T>) are accessible and applicable w.r.t. the test(() -> "test") method call. In such cases, when there are multiple matching methods, the compiler attempts to determine the most specific one. Now, while this determination for generic methods (as covered in JLS 15.12.2.5 and JLS 18.5.4) is quite complicated, we can use the intuition from the opening of 15.12.2.5:

The informal intuition is that one method is more specific than another if any invocation handled by the first method could be passed on to the other one without a compile-time error.

Since for any valid call to <T> void test(Supplier<T>) we can find a corresponding instantiation of the type parameter T in <T> void test(T), the former is more specific than the latter.

Now, the surprising part is that in your second example, both <T> void test(Class<T>, Supplier<T>) and <T> void test(Class<T>, T) are considered applicable for method call test(String.class, () -> "test"), even though it's clear to us, that the latter shouldn't be. The problem is, that the compiler acts very conservatively in the presence of implicitly typed lambdas, as quoted above. See in particular JLS 18.5.1:

A set of constraint formulas, C, is constructed as follows.

...

  • To test for applicability by strict invocation:

If k ≠ n, or if there exists an i (1 ≤ i ≤ n) such that e_i is pertinent to applicability (§15.12.2.2) (...) Otherwise, C includes, for all i (1 ≤ i ≤ k) where e_i is pertinent to applicability, ‹e_i → F_i θ›.

  • To test for applicability by loose invocation:

If k ≠ n, the method is not applicable and there is no need to proceed with inference.

Otherwise, C includes, for all i (1 ≤ i ≤ k) where e_i is pertinent to applicability, ‹e_i → F_i θ›.

and JLS 15.12.2.2:

An argument expression is considered pertinent to applicability for a potentially applicable method m unless it has one of the following forms:

  • An implicitly typed lambda expression (§15.27.1).

...

So, the constraints from implicitly typed lambdas passed as arguments take no part in resolving type inference in the context of method applicability checks.

Now, if we assume that both methods are applicable, the problem - and the difference between this and the previous example - is that none of this methods is more specific. There exist calls which are valid for <T> void test(Class<T>, Supplier<T>) but not for <T> void test(Class<T>, T), and vice versa.

This also explains why test(String.class, (Supplier<String>) () -> "test"); compiles, as mentioned by @Aominè in the comment above. (Supplier<String>) () -> "test") is an explicitly typed lambda, and as such is considered pertinent to applicability, the compiler is able to correctly deduce, that only one of these methods is applicable, and no conflict occurs.

这篇关于使用泛型和Lambda重载方法时模棱两可的方法调用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆