Почему этот универсальный код компилируется в Java 8?

Я наткнулся на фрагмент кода, который заставляет меня задуматься, почему он успешно компилируется:

public class Main {
    public static void main(String[] args) {
        String s =  newList(); // why does this line compile?
        System.out.println(s);
    }

    private static <T extends List<Integer>> T newList() {
        return (T) new ArrayList<Integer>();
    }
}

Что интересно, если я изменю сигнатуру методаnewList с<T extends ArrayList<Integer>> это больше не работает.

Обновление после комментариев и ответов: Если я переместу универсальный тип из метода в класс, код больше не будет компилироваться:

public class SomeClass<T extends List<Integer>> {
    public  void main(String[] args) {
        String s = newList(); // this doesn't compile anymore
        System.out.println(s);
    }

    private T newList() {
        return (T) new ArrayList<Integer>();
    }
}
 Tunaki29 июн. 2016 г., 13:19
 Periata Breatta16 янв. 2017 г., 22:46
Причина, по которой обновление имеет значение, заключается в том, что первая версия может работать, если существует какой-либо тип, удовлетворяющий ограничениям (T расширяет List <Integer> и T расширяет String) [т.е. это экзистенциальная квантификация], тогда как вторая версия может работать, только если все возможные типы T, которые соответствуют объявлению в классе, также расширяют String [т.е. это универсальное количественное определение.

Ответы на вопрос(3)

List это интерфейс. Если мы игнорируем тот факт, чтоString являетсяfinal на секунду, вы могли бы, теоретически, иметь класс, которыйextends String (то есть вы можете назначить егоs) ноimplements List<Integer> (имеется в виду, что он может быть возвращен изnewList()). Как только вы измените тип возврата из интерфейса (T extends List) к конкретному классу (T extends ArrayList) компилятор может определить, что они не присваиваются друг другу, и выдает ошибку.

Это, конечно, ломается, так какString на самом деле,finalи мы можем ожидать, что компилятор примет это во внимание. ИМХО, это ошибка, хотя я должен признать, что не являюсь экспертом по компиляции, и может быть веская причина игнорироватьfinal Модификатор в этой точке.

 Denis Rosca29 июн. 2016 г., 10:05
@errantlinguist Я не уверен, что следую тому, что вы говорите. Проблема заключается в том, что представленный код компилируется, когда я присваиваю значение типа T, расширяет List до значения типа String.
 Denis Rosca29 июн. 2016 г., 09:44
Хм, это действительно очень хороший момент. Я не думал о возможности иметь класс, который теоретически мог бы расширять как String, так и реализовать List. Как вы сказали, это не совсем применимо, поскольку String является окончательным, но это интересная идея.
 errantlinguist29 июн. 2016 г., 10:08
@DenisRosca, Хольгер только что объяснил, почему нет веских причин для предотвращенияT extends FinalClass.
 errantlinguist29 июн. 2016 г., 10:09
@DenisRosca, что касается моего предыдущего комментария, я попытался объяснить, почемуT extends String также следует принятьString.
 Denis Rosca29 июн. 2016 г., 10:40
@ Хольгер, что ты думаешь о втором примере кода? Если я переместу универсальный тип в класс вместо метода, код больше не будет компилироваться. ИМО это связано с тем, как вывод типов работает для универсальных методов.
 Holger29 июн. 2016 г., 11:18
Например, если вы вызываетеgetClass() по ссылке типа времени компиляцииX, вы получитеClass<? extends X> который может представлять подклассX, Это также относится, когдаX являетсяString, когда ты сказал"".getClass(), вы получитеClass<? extends String>, Позволяет конвертироватьClass<? extends String> вClass<String> просто такString являетсяfinal во время компиляции подорвал бы систему типов, еслиString не являетсяfinal во время выполнения.
 Holger29 июн. 2016 г., 11:16
Это компромисс между сложностью правил и безопасностью. Если вам было разрешено подклассfinal класс во время компиляции, вы, скорее всего, создадите класс, который сломается во время выполнения. Не допускается подклассfinal классы это простое правило. Если класс неfinal во время выполнения тот факт, что вы не создали подкласс, не причиняет вреда. Напротив, правила Generic довольно сложны.
 Denis Rosca29 июн. 2016 г., 10:23
@ Хольгер, вы знаете, где в спецификации я мог бы найти более подробное описание этого поведения? Разве компилятор не должен применять ту же логику кclass MyClass extends String затем?
 Holger29 июн. 2016 г., 10:05
Тот факт, что классfinal во время компиляции никогда не принимается во внимание в таких ситуациях, так как нет гарантии, что класс все еще будетfinal во время выполнения.
 errantlinguist29 июн. 2016 г., 10:07
Почему это должно быть «ошибкой»? - тип является экземпляром самого себя:String s = "blah"; System.out.println((s instanceof String)); распечатываетtrue, Поэтому казалось бы странным, что множество всех классовall SomeClass where (SomeClass instanceof SomeParentClass) == true будет исключатьSomeParentClass сам. По этой причине конкретный классSomeClass соответствует универсальному, определяя все его подтипы, например,List <T extends SomeClass, В конце концов,любой набор является подмножеством самого себя.

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

Так,newList() является универсальным методом, он имеет один параметр типа. Если вы укажете этот параметр, компилятор проверит это для вас:

Не в состоянии компилировать:

String s =  Main.<String>newList(); // this doesn't compile anymore
System.out.println(s);

Проходит этап компиляции:

List<Integer> l =  Main.<ArrayList<Integer>>newList(); // this compiles and works well
System.out.println(l);

Указание параметра типа

Параметры типа обеспечивают только проверку во время компиляции. Это по замыслу, Java используетстирание типа для универсальных типов. Чтобы заставить компилятор работать на вас, вы должны указать эти типы в коде.

Введите параметр при создании экземпляра

Наиболее распространенным случаем является указание шаблонов для экземпляра объекта. То есть для списков:

List<String> list = new ArrayList<>();

Здесь мы можем видеть, чтоList<String> определяет тип элементов списка. С другой стороны, новыйArrayList<>() не делает. Он используеталмазный оператор вместо. То есть компилятор Javaделает вывод, тип основан на объявлении.

Неявный параметр типа при вызове метода

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

public static <T extends Number> T max(T n1, T n2) {
    if (n1.doubleValue() < n2.doubleValue()) {
        return n2;
    }
    return n1;
}

Вы можете использовать это так:

int max = max(3, 4); // implicit param type: Integer

Или вот так:

double max2 = max(3.0, 4.0); // implicit param type: Double

Явные параметры типа при вызове метода:

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

List<Integer> noIntegers = Collections.<Integer>emptyList();

Параметр типа<Integer> передается в методemptyList(), Единственным ограничением является то, что вы также должны указать класс. То есть ты не сможешь это сделать:

import static java.util.Collections.emptyList;
...
List<Integer> noIntegers = <Integer>emptyList(); // this won't compile

Токен типа времени выполнения

Если ни один из этих приемов не поможет вам, вы можете указатьтокен типа времени выполнения, То есть Вы предоставляете класс в качестве параметра. Типичным примером являетсяEnumMap:

private static enum Letters {A, B, C}; // dummy enum
...
public static void main(String[] args) {
    Map<Letters, Integer> map = new EnumMap<>(Letters.class);
}
 Tamas Rev25 июл. 2016 г., 13:14
Простой ответ заключается в том, что вы можете указать параметры типа в пределах<> скобки. В противном случае я сделал объяснение дольше, чтобы вы могли увидеть все альтернативы.
 Krish Munot23 июл. 2016 г., 19:54
Не могли бы вы упростить объяснение?
Решение Вопроса

вы позволяете вызывающей стороне выбирать фактический тип для него, если этот фактический тип будет удовлетворять ограничениям. Этот тип не обязательно должен быть конкретным конкретным типом, это может быть абстрактный тип, переменная типа или тип пересечения, другими словами, более разговорные слова, гипотетический тип. Так,как сказал Мюрейник, может быть расширение типаString и внедрениеList, Мы не можем вручную указать тип пересечения для вызова, но мы можем использовать переменную типа, чтобы продемонстрировать логику:

public class Main {
    public static <X extends String&List<Integer>> void main(String[] args) {
        String s = Main.<X>newList();
        System.out.println(s);
    }

    private static <T extends List<Integer>> T newList() {
        return (T) new ArrayList<Integer>();
    }
}

Конечно,newList() не может удовлетворить ожидание возврата такого типа, но это проблема определения (или реализации) этого метода. Вы должны получить «непроверенное» предупреждение при кастеArrayList вT, Единственно возможная правильная реализация - возвращениеnull здесь, что делает метод совершенно бесполезным.

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

public class SomeClass<T extends List<Integer>> {
    public  void main(String[] args) {
        String s = newList(); // this doesn't compile anymore
        System.out.println(s);
    }

    private T newList() {
        return (T) new ArrayList<Integer>();
    }
}

Параметр type является частью контракта класса, поэтому тот, кто создает экземпляр, выберет фактические типы для этого экземпляра. Метод экземпляраmain является частью этого класса и должен подчиняться этому контракту. Вы не можете выбратьT ты хочешь; фактический тип дляT был установлен и в Java, вы обычно даже не можете узнать, чтоT является.

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

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

public class SomeClass<T extends List<Integer>> {
    public <X extends String&List<Integer>> void main(String[] args) {
        String s = new SomeClass<X>().newList();
        System.out.println(s);
    }

    private T newList() {
        return (T) new ArrayList<Integer>();
    }
}

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

Ваш ответ на вопрос