generic массивы и new T()

218
01 ноября 2018, 06:40

Java документация гласит, что джинерики работают с помощью механизма "стирания", т.е. это:

class Test<T> {
    T foo;
    T bar;
    Test(T o) {
    }
}

Будет превращено во время компиляции в это:

class Test {
    Object foo;
    Object bar;
    Test(Object o) {
    }
}

Ведь так? Хорошо, тогда почему такой код не работает?

class Test<T> {
    T[] foo;
    T bar;
    Test(T o) {
        foo = new T[5];
        bar = new T();
    }
}

Ведь, по идеи, этот код во время компиляции станет таким:

class Test {
    Object[] foo;
    Object bar;
    Test(Object o) {
        foo = new Object[5];
        bar = new Object();
    }
}

И никаких проблем быть не должно, но они есть. Почему так происходит?

Answer 1

Массивы отличаются от обобщённых типов в двух важных аспектах. Во-первых, массивы ковариантны (covariant). Это жуткое слово значит просто, что если Sub является подтипом Super, тогда тип массива Sub[] является подтипом Supeг[]. Средства обобщённого программирования, напротив, инвариантны (invariant): для любых двух отдельных типов Туре1 и Туре2 тип List не является ни подтипом, ни супертипом для List [JLS, 4.10; Naftalin07, 2.5]. Вы можете подумать, что это недостаток средств обобщённого программирования, но можно поспорить, что недостатком, напротив, обладают именно массивы.

Вторым важным отличием массивов от обобщённых типов является то, что массивы реифицированы (reified) [JLS, 4.7]. Это значит, что массивы знают и проверяют тип своих элементов при выполнении. Как выше было сказано, если вы попытаетесь сохранить String в массив Long, вы получите исключение ArrayStoreException. Обобщённые типы, напротив, реализуются стиранием (erasure) [JLS, 4.6]. Это значит, что они проверяют свои ограничения типов только на этапе компиляции и затем выбрасывают (или стирают) информацию о типах элементов при выполнении. Стирание позволяет обобщённым типам легко взаимодействовать со старым кодом, который не использует средства обобщённого программирования.

По причине этих фундаментальных различий массивы и обобщённые типы не могут применяться одновременно. Например, нельзя создавать массив обобщённых типов, параметризованных типов или параметров типа. Ни одно из этих выражений создания массивов не является разрешённым: new List[], new List[], new E[]. Все выражения приведут к ошибкам создания обобщённых массивов на этапе компиляции. Почему нельзя создавать обобщённые массивы? Потому что это небезопасно. Если бы это было разрешено, приведение типов, генерируемое компилятором в правильно написанной программе, могло бы вызвать исключение ClassCastException. Это бы нарушило фундаментальные гарантии, которые даёт система обобщённых типов.

© Joshua Bloch "Effective Java", глава 5, статья 25

Answer 2

T - это тип. Ключевое слово new вызывает конструктор класса. Но у типов нет ни методов, ни констукторов.

Ради интереса представим что new T() работало бы. Пусть у нас будет дженерик метод вызывающий конструктор внутри, и пусть будет класс User с нестандартным конструктором:

class User {
    String name;
    User(String name) {
        this.name = name;
    }
}
public class Test {
    public static void main(String[] args) {
        User user = Test.<User>genericFoo();
        System.out.println(user.name);
    }
    static <T> T  genericFoo() {
        return new T();
    }
}

Получается что дженерик метод вызывает конструктор который не существует в природе! К моему сожалению, в Джаве порой трудно различить типы и классы. Например, вызов статического метода класса выглядит так, будто метод вызывается из типа - Test.<User>genericFoo(). Но типы не имеют методов.

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

Рассмотрим типичный случай:

interface Animal {
    default String makeVoice() {
        return "Grrrr";
    }
}
class Dog implements Animal { }
class Cat implements Animal { }

1) Благодаря наследованию мы можем вызвать методы полученные по наследству.

2) Благодаря сабтайпингу мы можем положить "котов" в переменную для "животных".

public class Test2 {
    public static void main(String[] args) {
        String voiсe = new Dog().makeVoice();
        Cat cat = new Cat();
        Animal animal = cat;
    }
}

Другой пример, когда иерархия типов не совпадает с наследованием:

public class Test3 {
    public static void main(String[] args) {
        Dog[] dogs = {};
        Animal[] animals = dogs;
        LinkedList<Dog> dogsList = new LinkedList<Dog>();
        LinkedList<Animal> animalsList = dogsList; // тут не компилируется
    }
}

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

Листы (как и остальные дженерик классы) - инвариантны, т.е. так как лист собак не наследует лист животных, то и в иерархии типов - они не связаны. А значит присваивать их компилятор не разрешает.

Надеюсь этот опус немного помог различать типы и классы.

READ ALSO
Есть способы сделать цветной шрифт в терминале?

Есть способы сделать цветной шрифт в терминале?

Используя сторонние библиотеки, или нативные библиотеки Java, можно сделать цветной шрифт в терминале? Может что-то наподобие printf? И есть возможность...

162
слои Service Repository BestPractices Spring

слои Service Repository BestPractices Spring

интересует момент: для каждого репозитори - свой сервис? И потом уже создавать общий сервис, связывая другие сервисы, или же, все таки можно...

256
Room. Создание простейшего примера

Room. Создание простейшего примера

В чём ошибка и как можно исправить?

159
Не верный селект? sqlite

Не верный селект? sqlite

Пишу не большого бота с базой sqliteВ базе две таблицы: products и categories_to_subcategories

169