Немного более сложная концепция, но она связывает фундаментальные идеи в предыдущих статьях вместе и представляет более четкий образ понимания типов в Java.
В моей предыдущей статье о массиве я говорил об этой необходимости помещать вещи в коллекции. В Python в списке хранятся целые числа, строки и т.д. В Java примитивный массив хранит определенный тип объекта на основе объявления типа.
int[] arrayOfInts = new int[2]; String[] arrayOfStrings = new String[2];
Тогда я сказал, что в Python у вас, похоже, есть свобода помещать целые числа и строки в один и тот же контейнер. Есть еще одна степень свободы, которую я упустил. Сохраняя одинаковый тип содержимого в контейнере, в Python вы можете сначала создать “общий” список, а затем решить, будет ли он использоваться как список целых чисел или список строк. В Java примитивный массив создается с объявлением типа и впоследствии не изменяется. Вот почему нам нужны дженерики.
Эволюция
Предположим, у вас есть выдвижной ящик. Ящик может содержать много вещей. Будучи неорганизованным человеком, вы не хотите маркировать ящики, делая их специфичными только для определенных предметов (ящик для одежды, другой для файлов и т. Д.). В Java, поскольку вам нужно указать тип во время компиляции, вы можете использовать следующий подход:
Ящик может содержать Предмет.
class Drawer { Object obj; Drawer(Object obj) { this.obj = obj; } Object get() { return this.obj; } }
Теперь, когда вы можете создать ящик, вы можете понять, что компилятору, похоже, все равно, что вы кладете в ящик:
// store a shirt into the drawer Drawer drawer = new Drawer("shirt"); // store integers into the drawer Drawer drawer = new Drawer(123); drawer.get() // returns an "Object"
Как мне достать что-нибудь из ящика стола? Теперь все, что хранится в ящике, будет иметь тип Объект
. Всякий раз, когда вы достаете что-то из ящика, вы должны нести ответственность за преобразование типов. В Java тип времени компиляции определяет, что могут делать ваши объекты, по крайней мере, до выполнения. Это означает, что любой метод, вызывающий объект, извлеченный из ящика, должен быть совместим с типом. Если элемент имеет тип Объект
, то компилятор знает только то, что безопасно делать Объект
выравнивает такие вещи, как toString()
.
Drawer drawer = new Drawer("shirt"); Drawer drawer = new Drawer(123); // ERROR: incompatible types: .. Object cannot be converted to String String shirt = drawer.get(); // Error: ClassCastException Integer cannot be String String shirt = (String)drawer.get(); Drawer drawer = new Drawer("shirt"); // SAFE: type cast Object to String before assignment String shirt = (String)drawer.get();
Вышесказанное иллюстрирует две проблемы.
- Вы должны ввести тип элемента, извлеченного из ящика, в правильный тип для назначения/связывания других вызовов методов.
- Из-за пункта 1 вызов методов без типизации может привести к ошибке
не удается найти символ
. Это компилятор, говорящий: “Я не знал, что этот метод доступен для этого типа объектов!”
// ERROR: what I get out is an Object, which does not // have a 'length()' method new Drawer("string").get().length() // ERROR: order of casting is wrong (String)(new Drawer("string").get()).length() // SAFE ((String)new Drawer("string").get()).length()
Этот подход неуклюж, подвержен ошибкам и требует целенаправленной типизации каждый раз, когда вы что-то достаете из ящика.
Рождение генетики
Для решения проблем и требований, которые я упомянул в приведенном выше примере, а именно:
- Ящик должен быть достаточно гибким, чтобы превратиться в контейнер для одежды, файлов и т. Д. Без определения аналогичных классов, которые отличаются только типом.
- Выдвижной ящик должен позволять извлекать предметы и сохранять их тип, чтобы избежать принудительной типизации.
- При всем удобстве, ящик все равно должен в полной мере использовать компилятор для проверки типов, когда это возможно.
Которые приводят нас к следующим идеям:
- Не указывайте, какой тип предмета помещается в ящик во время “изготовления”.
- Обладайте свободой шаблонной системы. Поместите заполнители и во время фактического использования замените заполнители реальными значениями.
С дженериками,
- Включите параметры типа в определения классов/интерфейсов/методов.
- Параметры типа “заполнитель” находятся в разных областях в зависимости от того, где вы их разместили, например, тело класса/метода соответственно.
- Позже вы замените параметры типа конкретными аргументами типа, что приведет к проявлению полиморфизма.
Терминология: параметры типа и аргументы типа
// the "T" and "U" below are type parameters // generic class declaration class Car{} // generic method declaration public static void doThis(U item) {} // The "String" below is a type argument new Car ();
Ящик переопределен с помощью универсальных
class Drawer{ T obj; Drawer(T obj) { this.obj = obj; } T get() { return this.obj; } }
В приведенном выше определении класса мы имеем T
в <>
означает, что мы определяем “заполнитель”. Буква “T” тривиальна, вы можете изменить ее на другие строки, такие как “U”, “O” и т.д. В этом смысле вы можете рассматривать “T” как переменную, в ней будет храниться тип, который вы собираетесь указать позже. Обратите внимание, что у вас может быть несколько заполнителей.
Типичные имена параметров типа в Java
E | Элемент |
K | Ключ |
V | Ценность |
N | Номер |
T | Тип |
Теперь поиск объектов становится простым.
// PREVIOUSLY Drawer drawer = new Drawer("string"); ((String)drawer.get()).length() // CURRENT Drawerdrawer = new Drawer ("string"); drawer.get().length()
Следуя идеям параметров типов в определениях классов, мы можем сделать то же самое для методов.
// type parameter | return type | method name | method arguments Drawer customDrawer(U obj) { return new Drawer(obj); }
Для универсальных методов параметр типа ограничен самим методом. Если этот метод относится к универсальному классу, параметр типа метода не имеет ничего общего с параметрами типа класса.
Мы также можем поместить универсальный метод в универсальный класс. Опять же, обратите внимание, что два параметра типа находятся в разных областях.
class Drawer{ T obj; Drawer(T obj) { this.obj = obj; } T get() { return this.obj; } static Drawer customDrawer(U u) { return new Drawer(u); } } // calling the static method like this Drawer drawer = Drawer.customDrawer(123); drawer.get() // outputs 123
Автоматическая упаковка и распаковка
Обычно обсуждение того, как Java автоматически преобразует примитивные типы данных в ссылочные типы, вводится в начале курса Java. Дженерики пересматривают эту концепцию:
- Универсальные типы принимают только ссылочные типы в качестве аргументов типа.
// ERROR, does not accept primitive types Drawerdrawer = new Drawer (123);
- Универсальные шаблоны поддерживают стандартное поведение автоматической упаковки и распаковки между примитивными и ссылочными типами данных
// auto-boxing Drawerdrawer = new Drawer (123); // retrieve the Integer within the drawer Integer x = drawer.get(); // auto-unboxing int x = drawer.get();
Разнообразие типов
Ни сохраненный, ни перевернутый | Сохраненный | Отношение подтипа | Обратный |
Не бойтесь этих технических слов, давайте рассмотрим несколько примеров.
Ковариация
Связь подкласса может быть распространена на сложные типы данных, такие как массив.
// since Integer is a subtype/subclass of Object Object o = new Integer(123); // this relationship is preserved in Java arrays Object[] arr = new int[1];
Контравариантный
Противоположность ковариации. Будет подробно рассмотрено, когда мы обсудим подстановочные знаки в следующей статье 😂
Инвариантный
// ERROR Drawer
Обобщения инвариантны. Следовательно, аргументы типа, подставляемые в левой и правой частях знака равенства, всегда должны быть одинаковыми. Таким образом, мы можем опустить правую сторону Целое число
.
Drawerdrawer = new Drawer<>(123);
Java сможет определить правильный тип.
Следующий шаг: Подстановочные знаки 💨
P.S. Написано со ссылкой на лекцию NUS CS 2030 по генетике
Оригинал: “https://dev.to/tlylt/generics-oop-java-5-2k46”