【问题标题】:Java compiler: How can two methods with the same name and different signatures match a method call?Java 编译器:两个具有相同名称和不同签名的方法如何匹配一个方法调用?
【发布时间】:2019-05-24 14:37:08
【问题描述】:

我有这门课叫Container:

public class Container {

    private final Map<String, Object> map = new HashMap<>();

    public void put(String name, Object value) {
        map.put(name, value);
    }

    public Container with(String name, Object value) {
        put(name, value);
        return this;
    }

    public Object get(String name) {
        return map.get(name);
    }

    public <R> R get(String name, Function<Object, R> mapper) {

        Object value = get(name);

        if (null == value) {
            return null;
        }

        return mapper
            .apply(value);
    }

    public <R> R get(String name, Class<R> type) {

        Object value = get(name);

        if (null == value) {
            return null;
        }

        if (type.isAssignableFrom(value.getClass())) {
            return type
                .cast(value);
        }

        throw new ClassCastException(String
            .format("%s -> %s", value.getClass(), type));
    }
}

还有名为Token的类:

public class Token {

    private String value;

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public Token withValue(String value) {
        setValue(value);
        return this;
    }
}

最后是Token 类的测试类

public class TokenTest {

    @Test
    public void verifyToken() {
        verify("bar", new Token()
            .withValue("bar"));
    }

    @Test
    public void verifyContainer() {
        Container tokens = new Container()
            .with("foo", "bar")
            .with("baz", "bat");

        verify("bar", tokens.get("foo", String.class));
        verify("bat", tokens.get("baz", String::valueOf));  // line 21
    }

    private void verify(String expected, String actual) {
        verify(expected, new Token()
            .withValue(actual));
    }

    private void verify(String expected, Token actual) {
        Assert
            .assertEquals(expected, actual.getValue());
    }
}

测试仅在 eclipse 中编译和运行文件。

在命令行上构建时

mvn clean test

出现编译错误:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.0:testCompile (default-testCompile) on project ambiguous: Compilation failure
[ERROR] /C:/data/projects/java/ambiguous/src/test/java/ambiguous/TokenTest.java:[21,9] reference to verify is ambiguous
[ERROR]   both method verify(java.lang.String,java.lang.String) in ambiguous.TokenTest and method verify(java.lang.String,ambiguous.Token) in ambiguous.TokenTest match

当我将21 更改为其中之一时,编译也会失败

verify("bat", tokens.get("baz", e -> String.valueOf(e)));
verify("bat", tokens.get("baz", e -> e.toString));

当我将行更改为其中之一时

verify("bat", tokens.get("baz", String.class));
verify("bat", tokens.get("baz", Object::toString));

编译成功。

我无法理解为什么会出现这个编译错误。

我遇到了以下链接boxing and unboxingmultiple generic types and intersection types 和这个eclipse compiler bug,但我仍然无法与上述原因联系起来。

我的问题是,当映射器String::valueOf 传递给get 方法时,是什么让编译器认为verify 方法的两个签名都匹配?

使用以下 jdk 进行编译(使用 maven 和 gradle):

$ java -version
openjdk version "1.8.0_201-1-ojdkbuild"
OpenJDK Runtime Environment (build 1.8.0_201-1-ojdkbuild-b09)
OpenJDK 64-Bit Server VM (build 25.201-b09, mixed mode)

【问题讨论】:

  • 至少对我而言,没有一个变体可与使用 Java 12 的 IntelliJ 一起使用。如果在您执行 get 时类型绑定到方法而不是类 &lt;R&gt;,则相关部分。
  • related1related2.... 这就像一个递归 - 它可能对您来说是显而易见的,但对编译器来说却不是。 一般而言 lambda 形式的重载和多边形表达式很棘手

标签: java lambda java-8 method-reference


【解决方案1】:

根据JLS §15.12.2.2

参数表达式被认为与适用性相关 可能适用的方法m,除非它具有以下之一 表格:

  • 隐式类型的 lambda 表达式1.
  • 不精确的方法引用表达式2.
  • [...]

因此:

verify("bar", tokens.get("foo", e -> String.valueOf(e)));

在重载解析期间,从适用性检查中跳过了一个隐式类型的 lambda 表达式 e -&gt; String.valueOf(e) - 两个 verify(...) 方法都适用 - 因此存在歧义。

相比之下,以下是一些可行的示例,因为类型是明确指定的:

verify("bar", tokens.get("foo", (Function<Object, String>) e -> String.valueOf(e)));

verify("bar", tokens.get("foo", (Function<Object, String>) String::valueOf));

1 - 隐式类型的 lambda 表达式是一个 lambda 表达式,其中所有形式参数的类型都被推断出来。
2 - 不精确的方法引用 - 具有多个重载的方法引用.

【讨论】:

  • 使用 (Object e) -&gt; String.valueOf(e)) 显式键入 lambda 可解决编译错误。另一方面,这种行为是否意味着编译器认为 lambda 也可能返回 Token
  • @A4L 不,lambda 表达式只是从检查中跳过。
【解决方案2】:

String.valueOf(...) 有多种不同参数的实现。编译器不知道你想调用哪一个。编译器无法看到所有可能的方法实际上都返回String,因此调用哪个方法并不重要。由于编译器不知道返回类型是什么,它无法将正确的 Function&lt;...,...&gt; 推断为表达式的类型,因此它无法理解您是否会有 Function 或其他手头的东西,因此无法判断您是否想用FunctionClass 调用get 方法。


如果您使用e -&gt; String.valueOf(e) 而不是String::valueOf,那么编译器可以推断更多,但它仍然无法理解您将始终返回String,因此会将其解释为Function&lt;Object, Object&gt;,您的@987654332 @方法然后有问题。


e -&gt; e.toString我不完全理解,我不明白为什么编译器无法在这里推断String作为返回类型。它推断Object 并执行与前一个案例完全相同的操作。如果将操作拆分为

String s = tokens.get("baz", e -> e.toString());
verify("bat", s);  // line 21

然后它可以工作,因为编译器可以从s 的类型推断出通用R。与显式指定R 的工作方式相同:

verify("bat", tokens.<String>get("baz", e -> e.toString()));  // line 21

String.class 编译器很容易理解你要调用get(Class) 方法。


Object::toString 可以正常工作,因为编译器知道这将是 Function&lt;Object, String&gt;

【讨论】:

  • The compiler is not capable of seeing that all the possible methods actually return a String 但这是为什么呢?
  • @A4L 我不知道,但是编译器没有做很多事情,有些事情他们可以做但故意不做,而其他事情实际上是不可能的。就实际引用 JLS 而言,Oleksandr 的答案实际上可能更正确,我只是根据经验进行解释和发言。
猜你喜欢
  • 2018-11-04
  • 1970-01-01
  • 2013-11-26
  • 2020-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-21
  • 1970-01-01
相关资源
最近更新 更多