【问题标题】:Reference to method is ambiguous when using lambdas and generics使用 lambda 和泛型时对方法的引用不明确
【发布时间】:2015-03-28 22:47:55
【问题描述】:

我在以下代码中遇到错误,我认为它不应该存在...使用 JDK 8u40 编译此代码。

public class Ambiguous {
    public static void main(String[] args) {
        consumerIntFunctionTest(data -> {
            Arrays.sort(data);
        }, int[]::new);

        consumerIntFunctionTest(Arrays::sort, int[]::new);
    }

    private static <T> void consumerIntFunctionTest(final Consumer<T> consumer, final IntFunction<T> generator) {

    }

    private static <T> void consumerIntFunctionTest(final Function<T, ?> consumer, final IntFunction<T> generator) {

    }
}

错误如下:

Error:(17, 9) java: 对 consumerIntFunctionTest 的引用不明确 net.tuis.ubench.Ambiguous 中的方法 consumerIntFunctionTest(java.util.function.Consumer,java.util.function.IntFunction) 和 net 中的方法 consumerIntFunctionTest(java.util.function.Function,java.util.function.IntFunction) .tuis.ubench.Ambiguous 匹配

错误发生在以下行:

consumerIntFunctionTest(Arrays::sort, int[]::new);

我相信应该没有错误,因为所有Arrays::sort 引用都是void 类型的,并且它们都没有返回值。如您所见,当我显式扩展 Consumer&lt;T&gt; lambda 时,它确实工作。

这真的是 javac 中的一个错误,还是 JLS 声明 lambda 在这种情况下无法自动展开?如果是后者,我仍然认为这很奇怪,因为 consumerIntFunctionTest 与第一个参数 Function&lt;T, ?&gt; 不应该匹配。

【问题讨论】:

  • JLS 中应该定义的位置是15.27.3。 (没仔细看)。
  • 为什么你认为Function&lt;T, ?&gt; 不匹配? ? 也可以是 Void,所以它匹配。
  • 我会说它必须是某种错误:当注释掉 Consumer 方法时,它抱怨它可以not调用带有给定 lambda 的 Function- 方法 - 因此,无论如何它都不能模棱两可。有趣:当将参数声明为(int[] data)(因此,使其成为显式类型 lambda)时,它会正确地将其解析为Consumer 版本。在正文中另外插入return null; 时,它会解析为Function 版本。所以它显然在 implicitly typedvoid compatible lambda(如 JLS 中定义)上绊倒了。
  • 我得到了同样的错误,但由于 tomse 声明代码在 1.8.0_25 下编译,这可能是 1.8.0_40 特有的问题。或许尝试在1.8.0_25下运行看看代码是否编译?
  • @EddyG 鉴于您认为存在 javac 错误的时间与 实际上 的时间相比,我认为首先在 Stackoverflow 上写一个问题更合适,然后才写一个错误报告。作为记录,我确实在几个小时前提交了它,但我仍在等待它是否会被接受。

标签: java lambda java-8


【解决方案1】:

在你的第一个例子中

consumerIntFunctionTest(data -> {
        Arrays.sort(data);
    }, int[]::new);

lambda 表达式有一个void-compatible 块,可以通过表达式的结构来识别,而无需解析实际类型。

相比之下,在示例中

consumerIntFunctionTest(Arrays::sort, int[]::new);

必须解析方法引用以找出它是否符合void 函数(Consumer)或值返回函数(Function)。这同样适用于简化的 lambda 表达式

consumerIntFunctionTest(data -> Arrays.sort(data), int[]::new);

这可能是void- 兼容或值-兼容,这取决于解析的目标方法。

问题是解决方法需要知道所需的签名,这应该通过目标类型来确定,但是在知道泛型方法的类型参数之前,目标类型是未知的。虽然理论上两者都可以同时确定,但(仍然非常复杂)过程已在规范中进行了简化,因为首先执行方法重载解析,最后应用类型推断(请参阅JLS §15.12.2)。因此,类型推断可以提供的信息不能用于解决重载决议。

但请注意15.12.2.1. Identify Potentially Applicable Methods 中描述的第一步包含:

根据以下规则,表达式可能与目标类型兼容

  • 如果满足以下所有条件,则 lambda 表达式(第 15.27 节)可能与功能接口类型(第 9.8 节)兼容:

    • 目标类型的函数类型的元数与 lambda 表达式的元数相同。

    • 如果目标类型的函数类型返回 void,则 lambda 主体是语句表达式(第 14.8 节)或 void 兼容块(第 15.27.2 节)。

    • 如果目标类型的函数类型具有(非 void)返回类型,则 lambda 主体是表达式或值兼容块(第 15.27.2 节)。

  • 方法引用表达式(第 15.13 节)可能与函数接口类型兼容,如果类型的函数类型元数为 n,则至少存在一个可能适用于元数为 n 的方法引用表达式的方法(第 15.13 节)。 1),并且下列情况之一为真:

  • 方法引用表达式的格式为 ReferenceType :: [TypeArguments] Identifier,并且至少一个可能适用的方法是 i) 静态并支持 arity n,或 ii) 非静态并支持 arity n-1。

  • 方法引用表达式具有其他形式,并且至少有一个可能适用的方法不是静态的。

潜在适用性的定义超出了基本的数量检查,还考虑了功能接口目标类型的存在和“形状”。在某些涉及类型参数推断的情况下,作为方法调用参数出现的 lambda 表达式在重载决议之后才能正确键入

因此,在第一个示例中,其中一个方法是按 lambda 的形状排序的,而在方法引用或由唯一调用表达式组成的 lambda 表达式的情况下,两种可能适用的方法都会承受第一个选择过程并产生“类型推断之前出现“模糊”错误,以帮助查找目标方法以确定它是 void 还是返回值的方法。

请注意,就像使用 x-&gt;{ foo(); } 使 lambda 表达式明确地与 void 兼容一样,您可以使用 x-&gt;( foo() ) 使 lambda 表达式明确地值兼容。


您可以进一步阅读 this answer 解释说组合类型推断和方法重载解决方案的这种限制是一个深思熟虑(但不容易)的决定。

【讨论】:

  • 这是否也可以解释为什么如果我删除 &lt;T&gt; 类型参数并在两个参数中用 int[] 替换它,我会得到相同的错误?这似乎可以解决泛型问题,但仍然会出现该错误。
  • 如链接答案中所述(参见comparing 示例),在重载解析期间不考虑 lambda 表达式/方法引用的返回类型。请注意,最近的编译器会在声明站点向您发出有关重载方法的潜在歧义的警告,而无需通过实际的歧义调用来查找。您知道“目标类型未知”适用于编译器(遵循正式流程)而不适用于我们人类读者,并且不需要泛型,而只是解析步骤的严格顺序。
  • x-&gt;( foo() ) 解决方法很棒。但是,您能解释一下在这种情况下圆括号的确切含义吗?那是 lambda 语法吗?还是 java 语句的“正常”包装?
  • @mkurz 它是 表达式 的正常包装,因为 foo() 可以是表达式或语句,而 (foo()) 只能是表达式。例如,你可以写var result = (foo());,但你不能写(foo());作为声明。同样,{ foo(); } 只能是一个语句,因为您可以在需要语句的地方写它,但不能写 var result = { foo(); }
【解决方案2】:

使用方法引用,你可以有完全不同的参数类型,更不用说返回类型了,如果你有另一个方法与arity(参数的数量)匹配,仍然会得到这个。

例如:

static class Foo {

    Foo(Consumer<Runnable> runnableConsumer) {}

    Foo(BiFunction<Long, Long, Long> longAndLongToLong) {}
}

static class Bar {

    static void someMethod(Runnable runnable) {}

    static void someMethod(Integer num, String str) {}
}

Bar.someMethod() 不可能满足longAndLongToLong,但是下面的代码在歧义方面发出了相同的编译错误:

new Foo(Bar::someMethod);

Holger 的回答很好地解释了这背后的 JLS 中的逻辑和相关条款。

二进制兼容性如何?

考虑Foo 构造函数的longAndLongToLong 版本是否不存在但后来在库更新中添加,或者Bar.someMethod() 的两个参数版本不存在但后来添加:突然之前编译代码可能会因此而破裂。

这是方法重载的不幸副作用,甚至在 lambda 或方法引用出现之前,类似的问题就已经影响了普通方法调用。

幸运的是,保留了二进制兼容性。相关条款在13.4.23. Method and Constructor Overloading

添加重载现有方法或构造函数的新方法或构造函数不会破坏与预先存在的二进制文件的兼容性。每次调用使用的签名是在编译这些现有二进制文件时确定的; ....

虽然添加新的重载方法或构造函数可能会在下一次编译类或接口时导致编译时错误,因为没有最具体的方法或构造函数(第 15.12.2.5 节),但在以下情况下不会发生此类错误程序被执行,因为在执行时没有进行重载决议。

【讨论】:

    猜你喜欢
    • 2011-07-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-29
    相关资源
    最近更新 更多