Рубрики
Без рубрики

Дженерики – [ООП и Java #5]

Несколько более сложная концепция, но она связывает фундаментальные идеи в предыдущих статьях вместе и п… С тегами computer science, java, ооп.

Немного более сложная концепция, но она связывает фундаментальные идеи в предыдущих статьях вместе и представляет более четкий образ понимания типов в 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. Вы должны ввести тип элемента, извлеченного из ящика, в правильный тип для назначения/связывания других вызовов методов.
  2. Из-за пункта 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
Drawer drawer = 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
Drawer drawer = new Drawer(123);
  • Универсальные шаблоны поддерживают стандартное поведение автоматической упаковки и распаковки между примитивными и ссылочными типами данных
// auto-boxing
Drawer drawer = 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 drawer = new Drawer(123);

// SAFE
Drawer drawer = new Drawer(123);

Обобщения инвариантны. Следовательно, аргументы типа, подставляемые в левой и правой частях знака равенства, всегда должны быть одинаковыми. Таким образом, мы можем опустить правую сторону Целое число .

Drawer drawer = new Drawer<>(123);

Java сможет определить правильный тип.

Следующий шаг: Подстановочные знаки 💨

P.S. Написано со ссылкой на лекцию NUS CS 2030 по генетике

Оригинал: “https://dev.to/tlylt/generics-oop-java-5-2k46”