Часть 1. Подробное изучение Java Generics (0x01)

Часть 2. Подробное изучение Java Generics (0x02)

В последних двух главах мы обсудили настоятельную потребность сообщества Java ввести Generics Type и то, как это было окончательно реализовано с несколькими ограничениями из-за требования совместимости.

Конечно, это соответствовало цели введения дженериков с парой хитрых стираний типа. В частности, это вызвало волнение там, где система типов Java изначально не считалась Generics.

Таким образом, в этой статье мы подробно рассмотрим влияние системы типов Java, представив тип Generics и то, как он работает с существующими типами Java.

Дженерики ‹› Массив

И Generics, и Array по-прежнему принадлежат системе типов Java, поэтому они должны иметь возможность использовать в качестве базового типа для построения друг друга (Generics-Generics, Array-Array, Generics-Array, Array-Generics), например List<List<String>>, String[][] , List<String>[], List<String[]>.

Однако использование параметризованного типа для построения массива (который равен List<String>[]) не допускается из соображений безопасности типа (за исключением неограниченного подстановочного типа). Но как насчет List<String[]>, который использовал массив для создания параметризованного типа? Да, это разрешено. На самом деле большинство ссылочных типов в Java можно использовать в качестве фактического аргумента типа, за исключением примитивных типов.

Также выполним String[][] массива-массива, который мы назвали двумерным массивом, что означает, что мы можем создать новый тип массива с другим массивом. В идеале вы можете построить новый массив с N-размерами, но в Java было ограничение на размер массива, которое было Integer.MAX_VALUE.

Дисперсия

В прошлом посте мы говорили, что Java Array был одним из типов, обрабатывающих дисперсию, которые расширяли существующие отношения обычных типов Java до более сложных типов. Java Generics был точно таким же, как использование существующих типов для создания более сложного типа.

Поскольку Java Array является ковариантным типом, вы можете подумать, что Generics, конечно же, будут спроектированы как ковариантный тип. Ответ: да и нет. Он работает с Variance, но отличается от Java Array.

Дженерики ‹› Ковариация

Java разработала Array как ковариантный тип с некоторым компромиссом безопасности типов за счет задержки проверки типа во время выполнения.

Integer[] ints = new Integer[10];
Number[] nums = ints; //covariant
nums[0] = new Long(100L); // Will throw runtime expcetion: ArrayStoreException

Однако можно гарантировать безопасность типов массива во время компиляции. Давайте посмотрим, может ли Java ввести новое ключевое слово covariant, которое можно добавить перед определением типа и указать, что массив доступен только для чтения.

Integer[] ints = new Integer[10];
covariant Number[] nums = ints; //covariant
nums[0] = new Long(100L); // Will get compile error, such as `Number[]` is a covariant type that supports read-only.

Почему Java не справилась с этим так? Я думаю, что основной причиной является компромисс, который они выбрали, чтобы сделать массив действительно полезным в большинстве случаев. Например, у вас есть последовательность элементов, которые вы хотите поменять местами. Имейте в виду, что у вас не было фреймворка для хранения коллекций в том старом возрасте, единственной структурой, которая у вас была, был массив. Чтобы сделать этот метод действительно полезным для всех различных массивов, вы должны определить тип массива как object[]. Поэтому, если вы добавите к нему ограничение только для чтения, это будет бесполезно.

void swap(object[] items) {
    ...
}

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

<T> void swap(T[] itmes) {
    ...
}
...
Integer[] ints = new Integer[] {1, 2, 3};
String[] ss = new String[] {"a", "b", "c"};
swap(ints);
swap(ss);

Как в Java реализованы отношения супертипизации и подтипизации между дженериками? Можно ли его напрямую спроектировать как ковариантный массив?

Хорошо, давайте предположим, что Generics может быть напрямую ковариантным. Давайте напишем такой код,

ArrayList<Integer> intList = new ArrayList<>();
ArrayList<Number> numList = intList; //assume it is covariant.
numList.add(new Long(20)); //it is expected to have some exception such as CollectionStoreException like array did, but it won't be due to the type erasure.

В результате стирания типа нет ни проверки безопасности типа во время выполнения, ни проверки безопасности типа во время компиляции, поскольку мы допускаем ковариант между ArrayList<Integer> и ArrayList<Number>. Это создаст огромную проблему в системе типов Java и нарушит правила безопасности типов.

Так же, как мы обсуждали ранее, чтобы сделать тип Array безопасным, если разрешить ковариантность — его можно определить таким образом, чтобы он позволял только чтение, чтобы гарантировать безопасность типа. Java вводит синтаксис под названием upper bounded wildcard, подобный этому ArrayList<? extends Number>, чтобы сделать ArrayList<? extends Number> и ArrayList<Integer> ковариантными. Между тем, компилятор гарантирует доступ только для чтения при доступе к операциям в ArrayList<? extends Number>. Например, метод add() больше не сможет вызываться.

ArrayList<Integer> intList = new ArrayList<>();
ArrayList<? extends Number> numList = intList; //assume it is covariant.
numList.add(new Long(20)); //it is forbidded during compile.

Дженерики ‹› Контравариантность

Мы поговорили о ковариации, которая сохранит порядок типов (‹=) от более конкретного к более общему. А Контравариантность — это другая сторона, которая изменит порядок.

Что это означает? Чем это полезно?

Чтобы объяснить это более подробно, мы всегда должны возвращаться к основам системы подтипов в современных языках программирования.

В теории языков программирования подтипирование (также полиморфизм подтипов или полиморфизм включения) — это форма полиморфизма типов, в которой подтип — это тип данных, который связан с другим типом данных (супертипом) некоторым понятием заменяемости, что означает, что элементы программы , как правило, подпрограммы или функции, написанные для работы с элементами супертипа, также могут работать с элементами подтипа. — wikipedia.org

По сути, используя концепцию подтипов, язык программирования может легко позволить вам создать функцию, которая принимает объект определенного типа T, но также работает правильно, если передается объект, принадлежащий типу S, который является подтипом T.

Таким образом, и Ковариантность, и Контравариантность служат одной и той же цели: установлению отношений подтипов и использованию объектно-ориентированных функций языка.

Возьмем один пример. Предположим, вы хотите создать функцию, которая копирует все элементы из одного списка в другой список. Вы можете определить это так,

<T> void copy(List<T> source, List<T> dest) {
    ...
}

List<String> source = ...;
List<String> dest = ...;

copy(source, dest);

Однако вы можете копировать только те списки, которые имеют точно такие же аргументы типа. Например, вы не можете скопировать List<Integer> в List<Number>, даже если List<Number> безопасно принять элемент типа Integer.

Для этого параметр dest, определенный в методе copy, должен быть вариативным, чтобы он мог принимать свои подтипы. Мы только что обсудили один из способов, который называется Covariant. Применяя ковариацию к List<T> dest, вы получите List<? extends T> dest. Однако мы не ожидали, и на самом деле нам нужен обратный способ, которым параметр dest может принимать список, что его аргумент типа на самом деле является супертипом T. Например, A List<Integer> будет скопирован в место назначения, тип которого может быть List<Number>. А тем временем компилятор не разрешает операцию вставки в dest, которая не соответствует нашему требованию.

Это как раз тот случай, когда Contravariance спасает мир. Применяя Contravariance к dest, вы получите тип, подобный этому List<? super T> dest, который, кажется, позволяет вам передать List<Number> как подтип List<Integer>.

<T> void copy(List<T> source, List<? super T> dest) {
    ...
}

List<Integer> source = ...;
List<Number> dest = ...;

copy(source, dest); // the method signature equals to void copy(List<Integer> source, List<? super Integer> dest). Integer is the subtype of Number, but List<? super Integer> is the supertype of List<Number>.

Как вы могли заметить, безопасно добавлять элемент типа T или подтипа T в контравариантную коллекцию List<? super T>, но не для чтения. Это та же причина, что и ковариация: информация о типе компонента теряется после контравариантности, за исключением того, что тип компонента является супертипом T, поэтому безопасно добавить в него T или подтип T, но не чтение.

Первоначально опубликовано на https://jp-wang.github.io 10 октября 2019 г.