繁体   English   中英

使用 lambda 和泛型时对方法的引用不明确

[英]Reference to method is ambiguous when using lambdas and generics

我在以下代码中遇到错误,我认为不应该在那里...使用 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) {

    }
}

错误如下:

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

错误发生在以下行:

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

我相信应该没有错误,因为所有Arrays::sort引用都是void类型,并且它们都没有返回值。 正如您所观察到的,当我显式扩展Consumer<T> lambda 时它确实有效。

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

在你的第一个例子中

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

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

相比之下,在示例中

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 中描述的第一步。 确定可能适用的方法包括:

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

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

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

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

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

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

    • 方法引用表达式具有 ReferenceType :: [TypeArguments] 标识符的形式,并且至少一种可能适用的方法是 i) 静态并支持 n 元数,或 ii) 非静态且支持 n-1 元数。

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

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

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

请注意,就像使用x->{ foo(); } x->{ foo(); }使lambda表达式明确void兼容,可以使用x->( foo() )使lambda表达式明确值兼容。


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

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

例如:

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 中。 方法和构造函数重载

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM