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

Почему этот общий код компилируется в 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>();
    }
}
4b9b3361

Ответ 1

Если вы объявляете параметр типа в методе, вы позволяете вызывающему пользователю выбрать для него фактический тип, если этот фактический тип будет соответствовать ограничениям. Этот тип не должен быть конкретным конкретным типом, это может быть абстрактный тип, переменная типа или тип пересечения, в других, более разговорных словах, гипотетический тип. Итак, как сказал Mureinik, может существовать тип, расширяющий 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 вы должны получить предупреждение "unchecked". Единственная возможная правильная реализация будет возвращать здесь 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>();
    }
}

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

Ответ 2

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

Это, конечно, ломается, так как String, по сути, final, и мы могли ожидать, что компилятор учтет это. IMHO, это ошибка, хотя я должен признать, что я не эксперт-компилятор, и в этой точке может быть веская причина игнорировать модификатор final.

Ответ 3

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

Итак, 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);
}