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 expression1.
- An inexact method reference expression2.
- [...]
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>
.
来源:https://stackoverflow.com/questions/56294817/java-compiler-how-can-two-methods-with-the-same-name-and-different-signatures-m