具有多个匹配目标类型的 lambda 表达式的方法签名选择

2022-09-04 20:49:48

我正在回答一个问题,遇到了一个我无法解释的场景。请考虑以下代码:

interface ConsumerOne<T> {
    void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
}

class A {
    private static CustomIterable<A> iterable;
    private static List<A> aList;

    public static void main(String[] args) {
        iterable.forEach(a -> aList.add(a));     //ambiguous
        iterable.forEach(aList::add);            //ambiguous

        iterable.forEach((A a) -> aList.add(a)); //OK
    }
}

我不明白为什么显式键入lambda的参数会使代码编译。此外,为什么它链接到中的重载而不是中的重载?
对此是否有一些解释或指向规范相关部分的链接?(A a) -> aList.add(a)IterableCustomIterable

注意:仅在扩展时编译(完全重载方法会导致不明确的错误)iterable.forEach((A a) -> aList.add(a));CustomIterable<T>Iterable<T>CustomIterable


在两者上都得到这个:

  • openjdk 版本 “13.0.2” 2020-01-14
    Eclipse 编译器
  • openjdk 版本 “1.8.0_232”
    Eclipse compiler

编辑:上面的代码无法在使用maven构建时编译,而Eclipse成功编译最后一行代码。


答案 1

TL;DR,这是一个编译器错误。

没有规则可以在继承特定适用方法或默认方法时为其提供优先级。有趣的是,当我将代码更改为

interface ConsumerOne<T> {
    void accept(T a);
}
interface ConsumerTwo<T> {
  void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
    void forEach(ConsumerTwo<? super T> c); //another overload
}

该语句在 Eclipse 中产生错误。iterable.forEach((A a) -> aList.add(a));

由于在声明另一个重载时,接口中的方法的属性没有改变,Eclipse 选择此方法的决定不能(一致地)基于该方法的任何属性。它仍然是唯一继承的方法,仍然是唯一的方法,仍然是唯一的JDK方法,依此类推。无论如何,这两个属性都不会影响方法选择。forEach(Consumer<? super T) c)Iterable<T>default

请注意,将声明更改为

interface CustomIterable<T> {
    void forEach(ConsumerOne<? super T> c);
    default void forEach(ConsumerTwo<? super T> c) {}
}

还会产生“模棱两可”错误,因此适用的重载方法的数量也无关紧要,即使只有两个候选方法,也没有对方法的一般偏好。default

到目前为止,当有两种适用的方法并且涉及一种方法和继承关系时,问题似乎会出现,但这不是进一步挖掘的正确位置。default


但是可以理解的是,示例的构造可能由编译器中的不同实现代码处理,一个表现出错误,而另一个则没有。
隐式类型的 lambda 表达式,不能用于重载解析。相比之下,它是一个显式类型的lambda表达式,可用于从重载方法中选择匹配的方法,但它在这里没有帮助(在这里应该没有帮助),因为所有方法都具有具有完全相同的功能签名的参数类型。a -> aList.add(a)(A a) -> aList.add(a)

作为反例,与

static void forEach(Consumer<String> c) {}
static void forEach(Predicate<String> c) {}
{
  forEach(s -> s.isEmpty());
  forEach((String s) -> s.isEmpty());
}

功能签名不同,使用显式类型的 lambda 表达式确实可以帮助选择正确的方法,而隐式类型的 lambda 表达式则没有帮助,因此会产生编译器错误。所有Java编译器都同意这一点。forEach(s -> s.isEmpty())

请注意,这是一个不明确的方法引用,因为方法也是重载的,所以它也无法帮助选择一个方法,但无论如何,方法引用可能会由不同的代码处理。切换到一个明确的或更改为 ,以明确无误,并没有改变我的 Eclipse 安装中的结果(我使用过)。aList::addaddaList::containsListCollectionadd2019-06


答案 2

Eclipse 实现 JLS §15.12.2.5 的代码没有发现任何一种方法比另一种方法更具体,即使对于显式类型的 lambda 也是如此。

因此,理想情况下,Eclipse会在这里停下来报告模棱两可的情况。不幸的是,除了实现JLS之外,重载解析的实现还有重要的代码。根据我的理解,必须保留此代码(可追溯到Java 5还是新代码的时间)以填补JLS中的一些空白。

我已经提交了 https://bugs.eclipse.org/562538 来跟踪这一点。

除了这个特定的错误,我只能强烈建议不要使用这种代码风格。重载对于Java中的大量惊喜是有好处的,乘以lambda类型推断,复杂性与感知增益不成比例。


推荐