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


11

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

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)会使代码编译。另外,为什么它链接到in中的超载Iterable而不是in中的一个CustomIterable
是否对此有一些解释或指向规范相关部分的链接?

注意: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编译器

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


3
这三个都不是在Java 8上编译的。现在我不确定这是更新版本中修复的错误,还是引入的错误/功能...您可能应该指定Java版本
Sweeper

@Sweeper我最初使用jdk-13得到了它。Java 8(jdk8u232)中的后续测试显示了相同的错误。不知道为什么最后一个不能在您的计算机上编译。
ernest_k

两个在线编译器不能复制或者(12)。我在机器上使用1.8.0_221。这越来越奇怪了
扫频

1
@ernest_k Eclipse有其自己的编译器实现。这可能是该问题的关键信息。另外,我认为问题中还应强调一个干净的Maven构建错误,最后一行也是如此。另一方面,关于链接的问题,由于代码不可复制,因此可以澄清OP也使用Eclipse的假设。
Naman

2
虽然我了解该问题的知识价值,但我只能建议不要创建仅在功能接口上有所不同的重载方法,并期望可以安全地将其称为传递lambda。我认为,lambda类型推断和重载的结合是普通程序员几乎无法理解的。这是一个具有很多用户无法控制的变量的方程式。请避免这样做:)
Stephan Herrmann

Answers:


8

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
}

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

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

请注意,将声明更改为

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

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

到目前为止,当存在两种适用的方法并且default涉及一个方法和一个继承关系时,似乎就出现了问题,但这不是进一步探讨的正确位置。


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

作为反例,

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

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

请注意,这aList::add是一个模棱两可的方法引用,因为该add方法也被重载,因此它也无助于选择一个方法,但是无论如何方法引用可能会由不同的代码处理。切换到一个明确的aList::contains或改变ListCollection,做出add毫不含糊,并没有在我的Eclipse安装改变结果(我用的2019-06)。


1
@howlger,您的评论没有任何意义。默认方法继承的方法,并且该方法已重载。没有其他方法。继承的方法是一种default方法这一事实只是一个补充。我的回答也已经显示出一个例子,其中的Eclipse并没有为默认的方法给予优先。
Holger

1
@howlger我们的行为有根本的不同。您仅发现删除会default改变结果,并立即假设找到观察到的行为的原因。您对此过于自信,以至于您称其他答案错误,尽管事实甚至没有矛盾。因为您将自己的行为投射到其他行为上,所以我从未声称继承是原因。我证明不是。我证明了行为是不一致的,因为Eclipse在一种情况下选择了特定方法,而在存在三个重载的另一种情况下却没有选择。
Holger

1
@howlger除此之外,我已经叫在年底另一种情况下此评论,创建一个接口,没有继承,两种方法,一种default另外abstract,有两个消费者喜欢争论和尝试。Eclipse正确地说它是模棱两可的,尽管这是一种default方法。显然,继承仍然与该Eclipse错误相关,但是与您不同,我并没有发疯,并称其他答案错误,只是因为他们没有完全分析该错误。那不是我们的工作。
Holger

1
@howlger不,关键是这是一个错误。大多数读者甚至都不关心细节。Eclipse每次选择方法都可以掷骰子,这没关系。Eclipse在模棱两可的情况下不应选择一种方法,因此选择它的方法无关紧要。该答案证明该行为是不一致的,已经足以强烈表明它是错误。不必指出Eclipse源代码中哪里出了问题。那不是Stackoverflow的目的。也许,您是将Stackoverflow与Eclipse的错误跟踪器混淆了。
Holger

1
@howlger您再次错误地声称我对Eclipse为什么做出了错误选择做出了(错误)声明。再次,我没有,因为日食完全不应该做出选择。该方法是模棱两可的。点。之所以使用术语“ 继承 ”,是因为区分同名方法是必要的。我本来可以说“ 默认方法 ”,而无需更改逻辑。更正确地,我应该使用短语“ 由于某种原因Eclipse错误选择的非常方法 ”。您可以互换使用三个短语中的任何一个,并且逻辑不会改变。
Holger

2

Eclipse编译器正确地解析了该default方法,因为根据Java语言规范15.12.2.5,这是最具体的方法

如果确切地说是最大的特定方法中的一种是具体的(即非abstract或默认),则它是最特定的方法。

javac(默认情况下由Maven和IntelliJ使用)告诉此方法调用是不明确的。但是根据Java语言规范,它并不是模棱两可的,因为这两种方法之一是这里最具体的方法。

隐式类型的 lambda表达式的处理方式与Java中的显式类型的lambda表达式不同。与显式类型的lambda表达式相反,隐式类型属于第一阶段,以标识严格调用的方法(请参阅Java语言规范jls-15.12.2.2,第一点)。因此,这里的方法调用对于隐式类型的 lambda表达式是模棱两可的

就您而言,此错误的解决方法javac是指定功能接口类型,而不是使用如下所示的显式类型的lambda表达式:

iterable.forEach((ConsumerOne<A>) aList::add);

要么

iterable.forEach((Consumer<A>) aList::add);

这是您的示例,进一步将其最小化以进行测试:

class A {

    interface FunctionA { void f(A a); }
    interface FunctionB { void f(A a); }

    interface FooA {
        default void foo(FunctionA functionA) {}
    }

    interface FooAB extends FooA {
        void foo(FunctionB functionB);
    }

    public static void main(String[] args) {
        FooAB foo = new FooAB() {
            @Override public void foo(FunctionA functionA) {
                System.out.println("FooA::foo");
            }
            @Override public void foo(FunctionB functionB) {
                System.out.println("FooAB::foo");
            }
        };
        java.util.List<A> list = new java.util.ArrayList<A>();

        foo.foo(a -> list.add(a));      // ambiguous
        foo.foo(list::add);             // ambiguous

        foo.foo((A a) -> list.add(a));  // not ambiguous (since FooA::foo is default; javac bug)
    }

}

3
您错过了被引用句子之前的前提条件:“ 如果所有最大特定方法都具有等效替代签名 ”,当然,两个参数完全不相关的方法都没有等效替代签名。除此之外,这不能解释为什么default当三个候选方法或两个方法都在同一接口中声明时,Eclipse为什么停止选择该方法。
Holger

1
@Holger您的答案声称:“没有规则会在继承适用的特定方法或默认方法时为其赋予优先级。” 我是否正确理解您的意思是,您说的是此不存在的规则的先决条件在这里不适用?请注意,此处的参数是功能接口(请参阅JLS 9.8)。
豪尔格

1
您在上下文中撕了一个句子。该句子描述了替代等效方法的选择,换句话说,是在声明之间进行选择,所有声明都将在运行时调用同一方法,因为在具体类中将只有一个具体方法。这与类似的方法无关forEach(Consumer)forEach(Consumer2)这些方法永远不会以相同的实现方法结束。
Holger

2
@StephanHerrmann我不知道JEP或JSR,但是更改看起来像是一种修正,符合“具体”的含义,即与JLS§9.4进行了比较:“ 默认方法不同于具体方法(第8.4节。 3.1),它们在类中声明。”,它从未改变。
霍尔格

2
@StephanHerrmann是的,看起来像从替代等效方法中选择一个候选对象变得更加复杂,并且知道其背后的原理将很有趣,但这与当前的问题无关。应该有另一份文件解释变化和动机。过去曾经有过,但是有了“每½年更新一次”的政策,保持质量似乎是不可能的……
Holger

2

Eclipse实现JLS§15.12.2.5的代码没有找到任何一个方法比另一个方法更具体,即使对于显式键入的lambda也是如此。

因此,理想情况下,Eclipse将在此处停止并报告歧义。不幸的是,重载解决方案的实现除了实现JLS之外,还具有不平凡的代码。根据我的理解,必须保留此代码(从Java 5发行之日起),以填补JLS中的某些空白。

我已提交https://bugs.eclipse.org/562538进行跟踪。

独立于此特定的错误,我只能强烈建议不要使用这种代码风格。在Java中,重载可以带来很多惊喜,再加上lambda类型推断,那么复杂度与感知到的收益就成比例了。


谢谢。已经登录过bugs.eclipse.org/bugs/show_bug.cgi?id=562507了,也许您可​​以帮助链接它们或将其中一个关闭为重复...
ernest_k
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.