简体   繁体   中英

Java compiler: How can two methods with the same name and different signatures match a method call?

I have this class called 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));
    }
}

and the class called 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;
    }
}

and finally a test class for the Token class

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());
    }
}

The test compiles and runs just file in eclipse.

When building on the commad line

mvn clean test

a compile error is raised:

[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

The compilation also fails when I change line 21 to one of

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

When I change the line to one of

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

compilation is successful.

I cannot undestand why this compiliation error is raised.

I came across the follwong links boxing and unboxing , multiple generic types and intersection types and this eclipse compiler bug but I still cannot relate to the mentioned causes.

My question is, what makes the compiler think that both signatures of the verify method are matching when the mapper String::valueOf is passed to the get method?

For compilation the following jdk is used (with maven and 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)

According to the JLS §15.12.2.2 :

An argument expression is considered pertinent to applicability for a potentially applicable method m unless it has one of the following forms:

  • An implicitly typed lambda expression 1 .
  • An inexact method reference expression 2 .
  • [...]

Therefore:

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

an implicitly typed lambda expression e -> String.valueOf(e) is skipped from the applicability check during overload resolution - both verify(...) methods become applicable - hence the ambiguity.

In comparison, here are some examples that will work, because the types are specified explicitly:

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

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

1 - An implicitly typed lambda expression is a lambda expression, where the types of all its formal parameters are inferred.
2 - An inexact method reference - one with multiple overloads.

There are multiple implementations of String.valueOf(...) with different arguments. The compiler does not know which one you want to call. The compiler is not capable of seeing that all the possible methods actually return a String and therefore it does not really matter which method is called. Since the compiler does not know what the return type will be it cannot infer a proper Function<...,...> as the type of the expression and it therefore cannot understand wether you will have a Function or something else at hand and therefore cannot tell if you want to call the get method with a Function or a Class .


If you instead of String::valueOf use e -> String.valueOf(e) then the compiler can infer a little more but it will still not understand that you will always return a String and will therefore interpret it as Function<Object, Object> which your verify method then has a problem with.


e -> e.toString I do not understand fully, I do not see why the compiler is incapable of inferring String as a return type here. It infers Object and does the exact same thing as in the previous case. If you split the operation into

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

then it works because the compiler can infer the generic R from the type of s . The same way it works by explicitly specifying R :

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

String.class the compiler easily understands that you want to call the get(Class) method.


Object::toString makes sense to work since the compiler knows this will be a Function<Object, String> .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM