Почему автобокс делает некоторые вызовы неоднозначными в Java?

Сегодня я заметил, что автобокс иногда может вызывать неоднозначность в разрешении перегрузки метода. Простейший пример выглядит так:

public class Test {
    static void f(Object a, boolean b) {}
    static void f(Object a, Object b) {}

    static void m(int a, boolean b) { f(a,b); }
}

При компиляции это вызывает следующую ошибку:

Test.java:5: reference to f is ambiguous, both method
    f(java.lang.Object,boolean) in Test and method
    f(java.lang.Object,java.lang.Object) in Test match

static void m(int a, boolean b) { f(a, b); }
                                  ^

Исправление этой ошибки тривиально: просто используйте явный автобокс:

static void m(int a, boolean b) { f((Object)a, b); }

Который правильно вызывает первую перегрузку, как и ожидалось.

Так почему же не удалось разрешить перегрузку? Почему компилятор не автоматически блокирует первый аргумент и не принимает второй аргумент нормально? Почему я должен был явно запросить автобокс?

6 ответов

Решение

Когда вы сами приводите первый аргумент к Object, компилятор сопоставляет метод без использования autoboxing (JLS3 15.12.2):

Первая фаза (§15.12.2.2) выполняет разрешение перегрузки, не разрешая преобразование в коробку или распаковку или использование вызова метода переменной арности. Если на этом этапе не найдено подходящего метода, обработка продолжается до второго этапа.

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

Вторая фаза (§15.12.2.3) выполняет разрешение перегрузки, в то же время разрешая упаковывать и распаковывать, но все же исключает использование вызова метода переменной арности.

Почему на втором этапе компилятор не выбирает второй метод, потому что не требуется автобокс логического аргумента? Поскольку после того, как он нашел два метода сопоставления, только преобразование подтипа используется для определения наиболее специфического метода из этих двух, независимо от того, какой бокс или распаковка имели место, чтобы соответствовать им в первую очередь (§15.12.2.5).

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

public class Test {
    static void f(Object a, boolean b) {}
    static void f(int a, Object b) {}

    static void m(int a, boolean b) { f(a, b); } // ambiguous
}

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

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

На этой странице объясняются правила автобоксирования и выбора метода для вызова. Сначала компилятор пытается выбрать метод без использования какого-либо автобокса, потому что бокс и распаковка влекут за собой снижение производительности. Если ни один метод не может быть выбран без использования бокса, как в этом случае, тогда бокс находится на столе для всех аргументов этого метода.

Когда вы говорите f(a, b), компилятор не понимает, на какую функцию он должен ссылаться.

Это потому, что a является целым числом, но ожидаемый аргумент в f является объектом. Таким образом, компилятор решает преобразовать объект в объект. Теперь проблема в том, что если а можно преобразовать в объект, то можно и б.

Это означает, что вызов функции может ссылаться на любое из определений. Это делает вызов неоднозначным.

Когда вы конвертируете объект в объект вручную, компилятор просто ищет наиболее близкое совпадение и затем обращается к нему.

Почему компилятор не выбрал функцию, к которой можно обратиться, "выполнив минимально возможное количество преобразований в бокс / распаковку"?

Смотрите следующий случай:

f(boolean a, Object b)
f(Object a , boolean b)

Если мы вызываем как f(логическое a, логическое b), какую функцию он должен выбрать? Это неоднозначно, верно? Точно так же это станет более сложным, когда будет много аргументов. Поэтому компилятор выбрал вместо этого предупреждение.

Поскольку нет способа узнать, какую из функций на самом деле намеревался вызвать программист, компилятор выдает ошибку.

См. http://java.sun.com/docs/books/jls/third_edition/html/expressions.html

Приведение помогает, потому что тогда не требуется бокс для поиска метода для вызова. Без броска вторая попытка состоит в том, чтобы разрешить бокс, а затем можно также боксировать логическое значение.

Лучше иметь четкие и понятные спецификации, чтобы сказать, что произойдет, чем заставлять людей догадываться.

Так почему же не удалось разрешить перегрузку? Почему компилятор не автоматически блокирует первый аргумент и не принимает второй аргумент нормально? Почему я должен был явно запросить автобокс?

Второй аргумент обычно не принимается. Помните, что "логическое" также может быть помещено в объект. Вы могли бы явно привести логический аргумент к Object, и это сработало бы.

Компилятор Java разрешает перегруженные методы и конструкторы поэтапно. На первом этапе [§15.12.2.2] он определяет применимые методы путем подтипа [§4.10]. В этом примере ни один из методов не применим, потому что int не является подтипом Object.

На втором этапе [§15.12.2.3] компилятор идентифицирует применимые методы путем преобразования вызова метода [§5.3], которое является комбинацией автобоксирования и подтипирования. Аргумент int может быть преобразован в Integer, который является подтипом Object, для обеих перегрузок. Логический аргумент не нуждается в преобразовании для первой перегрузки и может быть преобразован в логический, подтип Object, для второго. Следовательно, оба метода применимы на втором этапе.

Поскольку применимо более одного метода, компилятор должен определить, какой из них наиболее специфичен [§15.12.2.5]. Он сравнивает типы параметров, а не типы аргументов, и не устанавливает их автоматически. Object и boolean - это не связанные типы, поэтому они считаются одинаково специфичными. Ни один метод не является более конкретным, чем другой, поэтому вызов метода неоднозначен.

Одним из способов устранения неоднозначности было бы изменение логического параметра на тип Boolean, который является подтипом Object. Первая перегрузка всегда будет более конкретной (если применимо), чем вторая.

Другие вопросы по тегам