Компилятор Java: как два метода с одинаковыми именами и разными сигнатурами могут соответствовать вызову метода?

У меня есть этот класс под названием 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));

компиляция прошла успешно.

Я не могу понять, почему возникает эта ошибка компиляции.

Я наткнулся на следующие ссылки: упаковка и распаковка, несколько универсальных типов и типов пересечения и эта ошибка компилятора eclipse, но я все еще не могу относиться к упомянутому причины.

Мой вопрос: что заставляет компилятор думать, что обе подписи метода verify совпадают, когда преобразователь String::valueOf передается методу get?

Для компиляции используется следующий 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)

person A4L    schedule 24.05.2019    source источник
comment
Ни один из вариантов не работает с IntelliJ с использованием Java 12, по крайней мере, для меня. Соответствующая часть, если тип привязан к методу, а не к классу <R>, пока вы выполняете get.   -  person Naman    schedule 24.05.2019
comment
related1 и related2.... здесь это похоже на рекурсию - это может быть очевидным для вас, но не для компилятора. Вообще перегрузки и полигональные выражения в виде лямбда-выражений сложны   -  person Eugene    schedule 24.05.2019


Ответы (2)


Согласно JLS §15.12 .2.2:

Выражение-аргумент считается имеющим отношение к применимости для потенциально применимого метода m, если только оно не имеет одну из следующих форм:

  • Неявно типизированное лямбда-выражение1.
  • Неточное выражение ссылки на метод2.
  • [...]

Следовательно:

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

неявно типизированное лямбда-выражение e -> 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 – Неявно типизированное лямбда-выражение – это лямбда-выражение, в котором выводятся типы всех его формальных параметров.
2 – Неточная ссылка на метод — одна с несколькими перегрузками.< / суп>

person Oleksandr Pyrohov    schedule 24.05.2019
comment
Явный ввод лямбды с помощью (Object e) -> String.valueOf(e)) устраняет ошибку компиляции. С другой стороны, означает ли такое поведение, что компилятор думает, что лямбда также может вернуть Token? - person A4L; 24.05.2019
comment
@ A4L Нет, лямбда-выражение просто пропускается при проверке. - person Oleksandr Pyrohov; 25.05.2019

Существует несколько реализаций String.valueOf(...) с разными аргументами. Компилятор не знает, какой из них вы хотите вызвать. Компилятор не способен увидеть, что все возможные методы на самом деле возвращают String, и поэтому на самом деле не имеет значения, какой метод вызывается. Поскольку компилятор не знает, каким будет тип возвращаемого значения, он не может вывести правильное Function<...,...> в качестве типа выражения и, следовательно, не может понять, есть ли у вас Function или что-то еще, и поэтому не может сказать, хотите ли вы вызвать get с Function или Class.


Если вы вместо String::valueOf используете e -> String.valueOf(e), то компилятор может вывести немного больше, но он все равно не поймет, что вы всегда будете возвращать String и поэтому интерпретирует его как Function<Object, Object>, с которым у вашего метода verify возникает проблема.


e -> e.toString Я не совсем понимаю, я не понимаю, почему компилятор не может вывести здесь String как возвращаемый тип. Он выводит Object и делает то же самое, что и в предыдущем случае. Если разделить операцию на

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

тогда это работает, потому что компилятор может вывести общий R из типа s. Точно так же это работает при явном указании R:

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

String.class компилятор легко понимает, что вы хотите вызвать метод get(Class).


Object::toString имеет смысл работать, поскольку компилятор знает, что это будет Function<Object, String>.

person luk2302    schedule 24.05.2019
comment
The compiler is not capable of seeing that all the possible methods actually return a String Но почему? - person A4L; 24.05.2019
comment
@ A4L Я не знаю, но есть много вещей, которые компиляторы не делают, некоторые вещи они могут делать, но намеренно не делают по сравнению с другими вещами, которые практически невозможны. Возможно, ответ Александра на самом деле более правильный с точки зрения фактической ссылки на JLS, мой - это просто интерпретация и рассказ из опыта. - person luk2302; 24.05.2019