为什么这个Java方法调用被认为是模棱两可的?

16

我遇到了一个奇怪的错误信息,我认为它可能是错误的。请考虑以下代码:

public class Overloaded {
    public interface Supplier {
        int get();
    }

    public interface Processor {
        String process(String s);
    }

    public static void load(Supplier s) {}
    public static void load(Processor p) {}

    public static int genuinelyAmbiguous() { return 4; }
    public static String genuinelyAmbiguous(String s) { return "string"; }

    public static int notAmbiguous() { return 4; }
    public static String notAmbiguous(int x, int y) { return "string"; }

    public static int strangelyAmbiguous() { return 4; }
    public static String strangelyAmbiguous(int x) { return "string"; }
}

如果我有一个长成这样的方法:

// Exhibit A
public static void exhibitA() {
    // Genuinely ambiguous: either choice is correct
    load(Overloaded::genuinelyAmbiguous); // <-- ERROR
    Supplier s1 = Overloaded::genuinelyAmbiguous;
    Processor p1 = Overloaded::genuinelyAmbiguous; 
}

我们得到的错误是很有道理的; load()的参数可以分配给任何一个, 所以我们会得到一个声明方法调用不明确的错误。

相反,如果我有一个看起来像这样的方法:

// Exhibit B
public static void exhibitB() {
    // Correctly infers the right overloaded method
    load(Overloaded::notAmbiguous);
    Supplier s2 = Overloaded::notAmbiguous;
    Processor p2 = Overloaded::notAmbiguous; // <-- ERROR
}

load()的调用很好,正如预期的那样,我不能将该方法引用分配给SupplierProcessor,因为它不是模糊的:Overloaded::notAmbiguous不能赋值给p2

现在是一个奇怪的例子。如果我有这样一个方法:

// Exhibit C
public static void exhibitC() {
    // Complains that the reference is ambiguous
    load(Overloaded::strangelyAmbiguous); // <-- ERROR
    Supplier s3 = Overloaded::strangelyAmbiguous;
    Processor p3 = Overloaded::strangelyAmbiguous; // <-- ERROR
}
编译器抱怨调用 load() 方法是不明确的(error: reference to load is ambiguous),但与A展示不同的是,我无法将方法引用分配给 SupplierProcessor。如果它真的是不确定的话,我感觉应该像在A展示中一样能够将s3p3分配给两个重载参数类型,但是我在 p3 上得到一个错误,指出error: incompatible types: invalid method reference。在C展示中的这第二个错误是有道理的,因为Overloaded::strangelyAmbiguous 无法被分配给 Processor,但如果它无法被分配,为什么它仍然被认为是模棱两可的呢?
看起来方法引用推断仅在函数接口的 arity 决定哪个重载版本要选择时才会看。在变量分配时,根据参数的数量和类型都会进行检查,这导致了在重载方法和变量分配之间存在差异。
对我来说,这似乎是一个缺陷。如果不是的话,至少错误消息是不正确的,因为在两个选择之间只有一个是正确的时候,可以说不存在歧义。

3
我可以再现,但请您能否删除“附件A”和“附件B”?我认为它们并没有真正增加对问题的理解。只有“附件C”本身就足够了。 - Andy Turner
4
记住,一个人必须维护这段代码。即使编译器可以弄清楚,我更喜欢像 loadProcessorloadSupplier 这样的名称,这样我就不必思考它们的含义了。 - markspace
1
请记住,作为人类,我们喜欢了解它的实际工作原理。 - Arnaud Claudel
3
@ArnaudClaudel 是的,我知道,这就是为什么这是一条评论,而不是答案。提问是可以的,但问题在很大程度上是学术性质的。希望没有人会用这种方式编写生产代码。 - markspace
2个回答

6
你的问题与这个问题非常相似。
简短回答如下:
Overloaded::genuinelyAmbiguous;
Overloaded::notAmbiguous;
Overloaded::strangelyAmbiguous;

所有这些方法引用都是不精确的(它们有多个重载)。因此,根据JLS §15.12.2.2。,在重载决策期间,它们会被跳过适用性检查,导致歧义。

在这种情况下,需要明确指定类型,例如:

load((Processor) Overloaded::genuinelyAmbiguous);
load(( Supplier) Overloaded::strangelyAmbiguous);

4
方法引用和重载,切勿混用。从理论上讲,您是正确的——这对于编译器来说应该非常容易推断,但让我们不要混淆人类和编译器。
编译器看到对“load”的调用并说:“嘿,我需要调用那个方法。好的,我可以吗?有两个。当然,让我们匹配参数。” 现在参数是对一个重载方法的方法引用。所以编译器在这里变得非常困惑,它基本上说:“如果我能告诉你指向哪个方法引用,我就可以调用load,但是,如果我能确定你想调用哪个load方法,我就可以推断出正确的strangelyAmbiguous”,因此它只是在追逐自己的尾巴。这个编译器"头脑"中的虚构决定是我能想到的最简单的解释方式。这带来了一个黄金坏习惯 - 方法重载和方法引用是一种糟糕的想法
但是,你可能会说——元数!当决定这是否为重载时,参数数量是编译器进行的第一件事情(可能)。这正是你关注的点:
Processor p = Overloaded::strangelyAmbiguous;

针对这种简单的情况,编译器确实可以推断出正确的方法,我的意思是我们人类可以做到,对于编译器来说也应该是容易的。问题在于,这只是一个包含2个方法的简单情况,那如果是100*100种选择呢?设计者们必须要么允许某些东西(比如说最多允许5*5并允许这种分辨率),要么完全禁止 - 我想你知道他们选择了哪种路径。如果使用lambda,显然会工作——arity很明确。

关于错误信息,如果您足够经常地使用lambda和方法引用,您将开始厌恶这种错误信息:“非静态方法无法从静态上下文中引用”,虽然与此完全无关。我记得这些错误消息从java-8开始有所改进,并不断提高,例如在java-15中,说不定这个错误消息也会更好。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接