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

Почему вызов типа Generic типа Java 8 выбирает эту перегрузку?

Рассмотрим следующую программу:

public class GenericTypeInference {

    public static void main(String[] args) {
        print(new SillyGenericWrapper().get());
    }

    private static void print(Object object) {
        System.out.println("Object");
    }

    private static void print(String string) {
        System.out.println("String");
    }

    public static class SillyGenericWrapper {
        public <T> T get() {
            return null;
        }
    }
}

Он печатает "String" под Java 8 и "Object" в Java 7.

Я ожидал, что это будет двусмысленность в Java 8, потому что оба перегруженных метода совпадают. Почему компилятор выбирает print(String) после JEP 101?

Обоснованный или нет, это нарушает обратную совместимость, и изменение не может быть обнаружено во время компиляции. Код просто скрытно ведет себя по-разному после перехода на Java 8.

ПРИМЕЧАНИЕ: SillyGenericWrapper поистине назван "глупым". Я пытаюсь понять, почему компилятор ведет себя так, как он это делает, не говорите мне, что глупая обертка - это плохой дизайн в первую очередь.

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

4b9b3361

Ответ 1

Правила вывода типа получили существенный пересмотр в Java 8; наиболее заметно, что вывод типа цели значительно улучшился. Итак, в то время как перед Java 8 сайт аргумента метода не получил никакого вывода, по умолчанию для Object, в Java 8 выводится наиболее специфический применимый тип, в данном случае String. В JLS для Java 8 была добавлена ​​новая глава Глава 18. Типовой вывод, отсутствующий в JLS для Java 7.

В более ранних версиях JDK 1.8 (до 1.8.0_25) была ошибка, связанная с разрешением перегруженных методов, когда компилятор успешно скомпилировал код, который, согласно JLS, должен был вызвать ошибку неоднозначности Почему этот метод перегружает неоднозначно? Как отмечает Marco13 в комментариях

Эта часть JLS, вероятно, самая сложная

который объясняет ошибки в более ранних версиях JDK 1.8, а также проблему совместимости, которую вы видите.


Как показано в примере из Java Tutoral (Type Inference)

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

void processStringList(List<String> stringList) {
    // process stringList
}

Предположим, вы хотите вызвать метод processStringList с пустым списком. В Java SE 7 следующий оператор не компилируется:

processStringList(Collections.emptyList());

Компилятор Java SE 7 генерирует сообщение об ошибке, подобное следующему:

List<Object> cannot be converted to List<String>

Для компилятора требуется значение для аргумента типа T, поэтому оно начинается со значения Object. Следовательно, вызов Collections.emptyList возвращает значение типа List, которое несовместимо с методом processStringList. Таким образом, в Java SE 7 вы должны указать значение значения аргумента типа следующим образом:

processStringList(Collections.<String>emptyList());

Это больше не требуется в Java SE 8. Понятие о том, что является типом назначения, было расширено, чтобы включить аргументы метода, такие как аргумент метода processStringList. В этом случае processStringList требует аргумент типа List

Collections.emptyList() - общий метод, подобный методу get(). В Java 7 метод print(String string) даже не применим к вызову метода, поэтому он не участвует в процессе разрешения перегрузки. Если в Java 8 применяются оба метода.

Эта несовместимость стоит упомянуть в Руководстве по совместимости для JDK 8.


Вы можете проверить мой ответ на аналогичный вопрос, связанный с перегруженными методами. Метод перегружает неоднозначность с помощью трехмерных термальных условных и unboxed-примитивов Java 8

Согласно 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).

Второй из трех вариантов соответствует нашему делу. Так как String является подтипом Object (String <: Object), он более конкретный. Таким образом, сам метод более конкретный. После JLS этот метод также строго конкретнее и наиболее специфичен и выбран компилятором.

Ответ 2

В java7 выражения интерпретируются снизу вверх (за очень немногими исключениями); значение подвыражения является своего рода "контекстом свободным". Для вызова метода типы аргументов разрешаются кулаком; компилятор затем использует эту информацию для разрешения значения вызова, например, чтобы выбрать победителя среди применимых перегруженных методов.

В java8 эта философия больше не работает, потому что мы ожидаем использовать неявную лямбду (например, x->foo(x)) всюду; типы параметров лямбда не указаны и должны быть выведены из контекста. Это означает, что для вызовов метода иногда типы параметров метода определяют типы аргументов.

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

Это серьезный сдвиг; и какой-то старый код, подобный вашему, станет жертвой несовместимости.

Обходной путь заключается в предоставлении "целевого ввода" аргумента с "контекстом каста"

    print( (Object)new SillyGenericWrapper().get() );

или как предложение @Holger, укажите параметр типа <Object>get(), чтобы избежать вывода всех вместе.


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

Ответ 3

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

Jls,. Раздел 15 содержит много информации о том, как именно компилятор выбирает перегруженный метод

Наиболее специфический метод выбирается во время компиляции; его дескриптор определяет, какой метод фактически выполняется во время выполнения.

Итак, при вызове

print(new SillyGenericWrapper().get());

Компилятор выбирает String версию поверх Object, потому что метод print, который принимает String, более конкретный, чем тот, который принимает Object. Если вместо String было Integer, то оно будет выбрано.

Кроме того, если вы хотите вызывать метод, который принимает Object в качестве параметра, вы можете назначить возвращаемое значение параметру типа Object E.g.

public class GenericTypeInference {

    public static void main(String[] args) {
        final SillyGenericWrapper sillyGenericWrapper = new SillyGenericWrapper();
        final Object o = sillyGenericWrapper.get();
        print(o);
        print(sillyGenericWrapper.get());
    }

    private static void print(Object object) {
        System.out.println("Object");
    }

    private static void print(Integer integer) {
        System.out.println("Integer");
    }

    public static class SillyGenericWrapper {
        public <T> T get() {
            return null;
        }
    }
}

Он выводит

Object
Integer

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

private static void print(Integer integer) {
    System.out.println("Integer");
}

private static void print(String integer) {
    System.out.println("String");
}

и теперь, если вы вызываете

print(sillyGenericWrapper.get());

У компилятора будет 2 допустимых метода определения, поэтому вы получите ошибку компиляции, потому что он не может отдать предпочтение одному методу над другим.

Ответ 4

Я запустил его с помощью Java 1.8.0_40 и получил "Object".

Если вы запустите следующий код:

public class GenericTypeInference {

private static final String fmt = "%24s: %s%n";
public static void main(String[] args) {

    print(new SillyGenericWrapper().get());

    Method[] allMethods = SillyGenericWrapper.class.getDeclaredMethods();
    for (Method m : allMethods) {
        System.out.format("%s%n", m.toGenericString());
        System.out.format(fmt, "ReturnType", m.getReturnType());
        System.out.format(fmt, "GenericReturnType", m.getGenericReturnType());   
   }

   private static void print(Object object) {
       System.out.println("Object");
   }

   private static void print(String string) {
       System.out.println("String");
   }

   public static class SillyGenericWrapper {
       public <T> T get() {
           return null;
       }
   }
}

Вы увидите, что вы получаете:

Объект public T com.xxx.GenericTypeInference $SillyGenericWrapper.get()               ReturnType: класс java.lang.Object        GenericReturnType: T

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