Предупреждение: метод [перегрузки] m1 потенциально двусмыслен с методом m2

import java.util.function.*;

class Test { 
    void test(int    foo, Consumer<Integer> bar) { }
    void test(long   foo, Consumer<Long>    bar) { }
    void test(float  foo, Consumer<Float>   bar) { }
    void test(double foo, Consumer<Double>  bar) { }
}

Когда я скомпилирую это с помощью javac -Xlint Test.java, я получаю пару предупреждений:

Test.java:4: warning: [overloads] test(int,Consumer<Integer>) in Test is potentially ambiguous with test(long,Consumer<Long>) in Test
    void test(int    foo, Consumer<Integer> bar) { }
         ^
Test.java:6: warning: [overloads] test(float,Consumer<Float>) in Test is potentially ambiguous with test(double,Consumer<Double>) in Test
    void test(float  foo, Consumer<Float>   bar) { }
         ^
2 warnings

Если изменить Consumer на Supplier, предупреждения исчезнут. Эта программа бесплатна:

import java.util.function.*;

class Test { 
    void test(int    foo, Supplier<Integer> bar) { }
    void test(long   foo, Supplier<Long>    bar) { }
    void test(float  foo, Supplier<Float>   bar) { }
    void test(double foo, Supplier<Double>  bar) { }
}

Почему? Что означает это предупреждение? Как эти методы неоднозначны? Безопасно ли пресекать предупреждение?

Ответ 1

Эти предупреждения возникают из-за веселого пересечения между разрешением перегрузки, типом ввода и типом вывода. Компилятор немного задумывается о вас и предупреждает вас, потому что большинство lambdas написаны без явно объявленных типов. Например, рассмотрите этот вызов:

    test(1, i -> { });

Каков тип i? Компилятор не может вывести его до тех пор, пока не завершит разрешение перегрузки... но значение 1 соответствует всем четырем перегрузкам. Независимо от того, какая перегрузка выбрана, это повлияет на целевой тип второго аргумента, который, в свою очередь, повлияет на тип, который был выведен для i. Здесь действительно недостаточно информации для компилятора, чтобы решить, какой метод вызывать, поэтому эта строка фактически приведет к ошибке времени компиляции:

    error: reference to test is ambiguous
           both method test(float,Consumer<Float>) in Test and
           method test(double,Consumer<Double>) in Test match

(Интересно, что он упоминает перегрузки float и double, но если вы прокомментируете один из них, вы получите ту же ошибку в отношении перегрузки long.)

Можно представить себе политику, в которой компилятор завершил разрешение перегрузки с использованием наиболее специфического правила, тем самым выбрав перегрузку с аргументом int. Тогда он будет иметь определенный целевой тип для применения к лямбда. Дизайнеры-компиляторы чувствовали, что это слишком сложно, и что будут случаи, когда программисты будут удивлены тем, что перегрузка закончилась. Вместо того, чтобы компилировать программы, возможно, непредвиденным образом, они чувствовали, что было бы безопаснее сделать это ошибкой и заставить программиста устранить эту проблему.

Компилятор выдает предупреждения в объявлениях метода, чтобы указать, что вероятный код, который программист написал бы для вызова одного из этих методов (как показано выше), приведет к ошибке времени компиляции.

Чтобы устранить неоднозначность вызова, вместо этого нужно было бы написать

    test(1, (Integer i) -> { });

или объявить какой-либо другой явный тип для параметра i. Другой способ - добавить бросок перед лямбдой:

    test(1, (Consumer<Integer>)i -> { });

но это, возможно, хуже. Вероятно, вы не хотите, чтобы вызывающие абоненты вашего API должны были бороться с такими вещами в каждой точке вызова.

Эти предупреждения не возникают для случая Supplier, так как тип Поставщика может быть определен с помощью локальных рассуждений без каких-либо выводов типа.

Вы, вероятно, захотите переосмыслить то, как вы объединяете этот API. Если вам действительно нужны методы с этими типами аргументов, вы можете переименовать методы testInt, testLong и т.д. И полностью перегрузить. Обратите внимание, что API-интерфейсы Java SE сделали это в подобных случаях, например comparingInt, comparingLong и comparingDouble на Comparator; а также mapToInt, mapToLong и mapToDouble на Stream.