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

2022-09-02 01:56:20

我在以下代码上收到错误,我认为不应该在那里...使用 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: 引用消费者IntFunctionTest是模棱两可的 两种方法消费者IntFunctionTest(java.util.function.Consumer,java.util.function.IntFunction) in net.tuis.ubench.Ambiguous and method consumerIntFunctionTest(java.util.function.Function,java.util.function.IntFunction) in net.tuis.ubench.ambiguous match

以下行发生错误:

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

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

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


答案 1

在第一个示例中

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

lambda 表达式具有一个 -兼容块,该块可以通过表达式的结构进行标识,而无需解析实际类型。void

相反,在示例中

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

必须解析方法引用才能确定它是否符合函数 () 或值返回函数 ()。这同样适用于简化的 lambda 表达式voidConsumerFunction

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

这可以是两者, - 兼容或值 - 兼容,具体取决于解析的目标方法。void

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

但请注意,15.12.2.1 中描述的第一步。确定可能适用的方法包括:

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

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

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

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

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

  • 方法引用表达式 (§15.13) 可能与功能接口类型兼容,如果该类型的函数类型 arity 为 n,并且存在至少一个可能适用于 arity n 的方法引用表达式 (§15.13.1),并且以下之一为真:

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

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

...

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

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

请注意,与使用使 lambda 表达式显式兼容一样,您可以使用 使 lambda 表达式显式与值兼容。x->{ foo(); }voidx->( 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背后的逻辑和相关条款。

二进制兼容性如何?

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

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

幸运的是,二进制兼容性得以保留。相关条款见13.4.23。方法和构造函数重载

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

虽然添加新的重载方法或构造函数可能会导致下次编译类或接口时出现编译时错误,因为没有最具体的方法或构造函数 (§15.12.2.5),但执行程序时不会发生此类错误,因为在执行时不会执行重载解析。


推荐