Подтвердить что ты не робот

Метод перегрузки неоднозначности с Java 8 тернарных условных и unboxed примитивов

Ниже приведены компиляции кода в Java 7, но не openjdk-1.8.0.45-31.b13.fc21.

static void f(Object o1, int i) {}
static void f(Object o1, Object o2) {}

static void test(boolean b) {
    String s = "string";
    double d = 1.0;
    // The supremum of types 'String' and 'double' is 'Object'
    Object o = b ? s : d;
    Double boxedDouble = d;
    int i = 1;
    f(o,                   i); // fine
    f(b ? s : boxedDouble, i); // fine
    f(b ? s : d,           i); // ERROR!  Ambiguous
}

Компилятор утверждает, что последний вызов метода неоднозначен.

Если мы изменим тип второго параметра f от int до Integer, тогда код будет компилироваться на обеих платформах. Почему компилируемый код не компилируется в Java 8?

4b9b3361

Ответ 1

Рассмотрим сначала упрощенную версию, которая не имеет тернарного условного выражения и не компилируется на Java HotSpot VM (build 1.8.0_25-b17):

public class Test {

    void f(Object o1, int i) {}
    void f(Object o1, Object o2) {}

    void test() {
        double d = 1.0;

        int i = 1;
        f(d, i); // ERROR!  Ambiguous
    }
}

Ошибка компилятора:

Error:(12, 9) java: reference to f is ambiguous
both method f(java.lang.Object,int) in test.Test and method f(java.lang.Object,java.lang.Object) in test.Test match

Согласно JLS 15.12.2. Время компиляции Шаг 2: определение подписи метода

Метод применим, если он применим одним из строгих вызовов (§15.12.2.2), свободным вызовом (§15.12.2.3) или вызовом переменной arity (§15.12.2.4).

Вызов связан с контекстом вызова, который объясняется здесь JLS 5.3. Контексты вызова

Если для вызова метода не задействован бокс или unboxing, применяется строгий вызов. Когда бокс или unboxing задействованы для вызова метода, применяется функция free invocation.

Идентификация применимых методов делится на 3 фазы.

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

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

Третья фаза (§15.12.2.4) позволяет комбинировать перегрузку с методами переменной arity, боксом и распаковкой.

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

Согласно JLS 15.12.2.5 Выбор наиболее конкретного метода:

Если более чем один метод-член доступен и применим к необходимо вызвать один из них, чтобы обеспечить дескриптор для отправки времени выполнения. Программирование на Java язык использует правило, в котором выбран наиболее специфический метод.

Тогда:

Один применимый метод m1 более конкретный, чем другой применимый метод m2, для вызова с выражениями аргументов e1,..., ek, if любое из следующего верно:

  • m2 является общим, а m1 считается более конкретным, чем m2 для выражения аргументов e1,..., ek по §18.5.4.

  • m2 не является общим, а m1 и m2 применимы строгими или свободными вызов и где m1 имеет формальные типы параметров S1,..., Sn и m2 имеет формальные типы параметров T1,..., Tn, тип Si более специфичен чем Ti для аргумента ei для всех я (1 ≤ я ≤ n, n = k).

  • m2 не является общим, а m1 и m2 применимы переменной arity invocation и где первые k переменных параметров arity-типов m1 представляют собой S1,..., Sk и первые k переменных параметров параметров arty m2 T1,..., Tk, тип Si более специфичен, чем Ti для аргумента ei для всех я (1 ≤ я ≤ k). Кроме того, если m2 имеет k + 1 параметров, то k + 1-й тип параметра переменной arity m1 является подтипом k + 1'-тип переменной переменной arity m2.

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

Тип S более специфичен, чем тип T для любого выражения, если S <: T (§ 4.10).

Может показаться, что второе условие соответствует этому случаю, но на самом деле это не потому, что int не является подтипом Object: это не так, что int <: Object. Однако, если мы заменим int целым числом в сигнатуре f-функции, это условие будет соответствовать. Обратите внимание, что первый параметр в методах соответствует этому условию, так как Object <: Object is true.

Согласно $4.10 не определено отношение подтипа/супертипа между примитивными типами и типами класса/интерфейса. Таким образом, int не является подтипом Object, например. Таким образом, int не является более конкретным, чем Object.

Так как среди 2 методов нет более конкретных методов, таким образом, не может быть строго более конкретного и не может быть не самый конкретный метод (JLS дает определения для тех терминов в том же абзаце JLS 15.12.2.5 Выбор наиболее конкретного метода). Таким образом, оба метода максимально специфичны.

В этом случае JLS предоставляет 2 варианта:

Если все максимально конкретные методы имеют эквивалентно-эквивалентные сигнатуры (§8.4.2)...

Это не наш случай, поэтому

В противном случае вызов метода неоднозначен и возникает ошибка времени компиляции.

Ошибка времени компиляции для нашего случая выглядит в соответствии с JLS.

Что произойдет, если мы изменим тип параметра метода из int в Integer?

В этом случае оба метода по-прежнему применимы при свободном вызове. Однако метод с параметром Integer более специфичен, чем метод с 2 объектными параметрами, поскольку Integer <: Object. Метод с параметром Integer строго конкретнее и наиболее специфичен, поэтому компилятор выберет его и не выбросит ошибку компиляции.

Что произойдет, если мы изменим double на Double в этой строке: double d = 1.0;?

В этом случае для строгого вызова используется ровно 1 метод: для вызова этого метода не требуется бокс или unboxing: f (Object o1, int i). Для другого метода вам нужно сделать бокс для значения int, чтобы он применим по свободному вызову. Компилятор может выбрать метод, применяемый при строгом вызове, поэтому ошибка компилятора не возникает.

Как отметил Марко13 в своем комментарии, в этом сообщении рассматривается аналогичный случай Почему этот метод перегружает неоднозначно?

Как объяснялось в ответе, произошли некоторые существенные изменения, связанные с механизмами вызова метода между Java 7 и Java 8. Это объясняет, почему код компилируется в Java 7, но не в Java 8.


Теперь наступает интересная часть!

Пусть добавляется тернарный условный оператор:

public class Test {

    void f(Object o1, int i) {
        System.out.println("1");
    }
    void f(Object o1, Object o2) {
        System.out.println("2");
    }

    void test(boolean b) {
        String s = "string";
        double d = 1.0;
        int i = 1;

        f(b ? s : d, i); // ERROR!  Ambiguous
    }

    public static void main(String[] args) {
        new Test().test(true);
    }
}

Компилятор жалуется на неоднозначный вызов метода. JLS 15.12.2 не требует каких-либо специальных правил, связанных с тернальными условными операторами при выполнении вызовов метода.

Однако существуют JLS 15.25 Условный оператор?: и JLS 15.25.3. Условные условные выражения. Первая классифицирует условные выражения в 3 подкатегории: логическое, числовое и ссылочное условное выражение. Второй и третий операнды нашего условного выражения имеют типы String и double соответственно. Согласно JLS, наше условное выражение является условным условным выражением.

Затем в соответствии с JLS 15.25.3. Условные условные выражения наше условное выражение является условным условным выражением, поскольку оно появляется в контексте вызова. Таким образом, тип нашего условного выражения представляет собой Object (целевой тип в контексте вызова). Отсюда мы могли бы продолжить шаги, как если бы первым параметром был Object, и в этом случае компилятор должен выбрать метод с int как второй параметр (и не выдавать ошибку компилятора).

Сложная часть - это примечание от JLS:

его второе и третье выражения операнда аналогично отображаются в контексте одного и того же типа с типом цели T.

Из этого мы можем предположить (также подразумевает это слово "poly" в названии), что в контексте вызова метода 2 операнда следует рассматривать независимо. Это означает, что когда компилятор должен решить, нужна ли операция бокса для такого аргумента, он должен изучить каждый из операндов и посмотреть, может ли понадобиться бокс. В нашем конкретном случае String не требует бокса, а двойной - бокса. Таким образом, компилятор решает, что для обоих перегруженных методов он должен быть свободным вызовом метода. Дальнейшие шаги такие же, как в случае, когда вместо тернарного условного выражения мы используем двойное значение.

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

Интересно, что моя IDE (IntelliJ IDEA) не обнаруживает последний случай (с тернарным условным выражением) как ошибку компилятора. Все остальные случаи, которые он обнаруживает в соответствии с java-компилятором от JDK. Это означает, что либо java-компилятор JDK, либо внутренний анализатор IDE имеют ошибку.

Ответ 2

Короче:

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

Когда вы используете Integer вместо int, компилятор выбирает метод с Integer, потому что Integer является подтипом Object.

Когда вы используете Double вместо double, компилятор выбирает метод, который не связан с боксом или распаковкой.

До Java 8 правила были разными, поэтому этот код мог компилироваться.