Что такое Generic programming?
Generic types (Универсальные типы)
Без Generic types – чем плохо?
Кто и как использует/не использует generic types?
Определения
Пример Generic-класса и его использования
Raw types
Универсальные (generic) методы
Ограничения на переменные типа
Множественные верхние границы
Generic-типы и наследование
Правила наследования. Примеры
Наследование из generic класса
Type inference (выводимость типа)
Подстановочные типы
Непривычный новый синтаксис
Синтаксический «дискомфорт»
Wildcards (подстановочные знаки, «джокеры»)
Bounded Wildcards (тут начинается «веселье»…)
Надо как-то «ограничить» используемые типы…
Пример графа наследования (пусть есть указанный справа DAG)
Ограниченные подстановочные типы
PECS (Producer Extends, Consumer Super) – Буратино в стране дураков?
Терминология (встречается в Java, Scala и др.)
Терминология
Пример наследования: Upper Bounded Wildcard types
Универсальный код и Java VM
Преобразование выражений
Преобразование методов
Надо помнить, что:
Ограничения при работе с generic типами
Такие классы НЕ могут быть generic:
Рекомендации по использованию (Дж.Блох)
Аннотация @SuppressWarnings(“unchecked”)
Расширение средств reflection для generics
Заключение
719.50K
Category: programmingprogramming

Универсальные и параметризованные типы (Generic programming)

1.

Программирование на Java
(2022 - 2023)
Лекция 7
Универсальные и параметризованные типы
(Generic programming)
к.т.н. Гринкруг Е.М. (email: [email protected])
19-Dec-22
Software Engineering
1

2. Что такое Generic programming?

• Generic programming – (обобщенное или универсальное программирование) способ написания кода, который может быть использован для
объектов различных типов
В языках с динамическим контролем типа (Smalltalk, Python, … ) – все
программы generic:
– Переменные не сопоставляются с типом значений, и нет нужды писать разный код
для разных типов данных;
– Ловля ошибок откладывается до времени исполнения...
В C++ :
– нет ничего похожего на общий суперкласс Object,
– все обобщенное программирование поддерживается с помощью templates (и
Standard Template Library by Александр Степанов)
В Java:
– до JDK 5 было больше похоже на Smalltalk,
– начиная с JDK 5 – с появлением параметризованных типов – стало больше похоже
на C++...
19-Dec-22
Software Engineering
2

3. Generic types (Универсальные типы)

• Добавлены в язык Java в версии JDK 5.0 (это 2004 г.);
– Разрабатывались до этого около 5 лет (с большими спорами, было и есть (!) о чем...);
• Позволяют создавать более надежный и читаемый (можно спорить ) код;
• Сокращают число переменных, декларируемых с типом Object, и число
требуемых операций приведения типов (downcast’ов);
• Особенно полезны при работе с коллекциями (наборами данных);
• Позволяют писать код, который можно повторно использовать для
работы с разнородными объектами;
• Позволяют компилятору находить ошибки «на ранней стадии» (до
исполнения программы – в статике);
• Их нужно понимать, хотя бы потому, что в современных JDK без этого
понимания затруднительно читать исходный код и/или API, которым
надо пользоваться (они нужны не только «писателям», но и «читателям»)
19-Dec-22
Software Engineering
3

4. Без Generic types – чем плохо?

• «Универсальные» типы (т.е. код, способный работать с многими типами
данных) можно реализовать только благодаря наследованию:
– при этом надо использовать везде ссылки на экземпляры Object;
• Есть две проблемы, если всегда иметь дело с Object’ами:
– чтобы достать данные для обработки из их хранилища, надо делать
downcast, например: String name = (String) myList.get(0);
– Нельзя проверить на наличие ошибки при занесении в хранилище;
проверить можно только потом, при извлечении и приведении типа.
Используя Generic types:
• Можно применять параметры типа; с их помощью можно явно указать
тип элементов в хранилище (например, в коллекции) и:
– не делать лишние downcast’ы при извлечении объектов;
– контролировать компилятором тип заносимых объектов (в статике).
19-Dec-22
Software Engineering
4

5. Кто и как использует/не использует generic types?

• Разработчики–пользователи готовых (старых) библиотек, в которых не
пользуются универсальными типами, и пользователи, которые
работают «по-старинке», могут столкнуться со сложно отыскиваемыми
ошибками, так как проявление ошибки может быть «далеко» от места
ее возникновения;
• Разработчики–пользователи, использующие готовые универсальные
типы, – поступают более «профессионально»...
– это не значит, что до JDK 5 (~ лет 8) вообще не было профессионалов ;
• Разработчики-пользователи, которым может понадобиться написать
свои универсальные типы и методы (для этого нужна хорошая
квалификация и практика).
• На
практике
использование
параметризованных
типов
оправдано в программах, где часто делается downcast.
19-Dec-22
Software Engineering
5

6. Определения

• Универсальный (generic) тип – это тип, имеющий формальные
параметры – типы, то есть:
– Это ссылочный тип, имеющий один или более параметров-типов,
имена которых перечисляются за именем типа в угловых скобках < >;
– Эти типы-параметры потом заменяются типами-аргументами
(фактическими типами-параметрами, ими не могут быть примитивные
типы), когда generic - тип инстанциируется.
• Параметризованный тип – это экземпляр универсального (generic)
типа с [уже подставленными] фактическими параметрами-типами.
• Пример:
public interface Collection<E> extends Iterable<E> {...}
Collection<String> collection = new LinkedList<String>();
// или просто new LinkedList<>(); начиная с JDK7
Вопрос: где тут универсальные и где параметризованные типы?
Принято давать типам-параметрам короткие имена и писать их заглавными буквами.
Часто: Е - тип элемента, K,V – типы ключей и значений, T, U, S – произвольные типы
19-Dec-22
Software Engineering
6

7. Пример Generic-класса и его использования

Простейшее хранилище данных (контейнер одного значения, переменная с
одним типизированным значением). (Как бы вы это сделали иначе?)
public class Variable<T> {
private T value;
public Variable() { }
public Variable(T param) {value = param;}
public void setValue(T newValue) { value = newValue; }
public T getValue() { return value; }
}
// использование (при <> фактические типы берутся слева - type inference, JDK7(+)):
Variable<String> stringVariable = new Variable<>(); // или Variable<String>();
Variable<Float> floatVariable = new Variable<>(); // или Variable< Float >();
stringVariable.setValue (“it’s OK”);
floatVariable.setValue (new Float (5.5F) ); // ok, но не наоборот...
19-Dec-22
Software Engineering
7

8. Raw types

• Если фактические параметры-типы для generic-типа отсутствуют, у нас
получается использование “raw” (“сырого”, “голого”) типа:
Variable rawVariable = new Variable();
– Здесь Variable – это raw-тип для generic-типа Variable<T> (с предыдущего слайда)
• Однако, non-generic тип (класс или интерфейс) не является raw-типом
generic-типа. Для обратной совместимости (с кодом в стиле до JDK5)
разрешено присваивание параметризованного типа его raw-типу:
Variable<String> stringVariable = new Variable<>(“qu-qu”);
rawVariable = stringVariable; // it’s ok…
НО: Variable rawVariable1 = new Variable();
Variable<Integer> integerVariable = rawVariable1; // warning: unchecked conversion
• Аналогичная ситуация имеет место при использовании raw-типа для
вызова generic-метода от generic-типа: rawVariable.seValue(7); …
19-Dec-22
Software Engineering
8

9. Универсальные (generic) методы

• Можно определять методы с параметрами-типами, которые в методах
используются для определения типов аргументов и результатов:
public <T, U, S> T crazyMethod (U param1, S param2) { … }
• Типы-параметры (типовые переменные) метода указываются перед
возвращаемым значением (и перечисляются там в угловых скобках);
• Универсальные методы могут быть в любых классах (не обязательно
generic); (BTW, а в каких классах могут быть абстрактные методы?)
• При вызове универсального метода перед его именем задают
реальные типы (фактические типы параметризованного метода):
String t = this. <String, Integer, Float> crazyMethod(5, 2.3F));
• Часто компилятор сам понимает, какие типы подставлять по типам
переданных параметров и другой информации (type inference).
19-Dec-22
Software Engineering
9

10. Ограничения на переменные типа

Рассмотрим пример (мы хотим иметь
универсальный код):
public static <T> T min(T[] array) {
if(array == null || array.length == 0)
return null;
T smallest = array[0];
for(int i = 1; i < array.length; i++)
if (smallest.compareTo(array[i]) > 0)
smallest = array[i];
return smallest;
}
public interface Comparable<T> {
public int compareTo (T o);
}
19-Dec-22
Переменная smallest имеет тип T,
но кто сказал, что у Т есть метод
compareTo() ?
Надо ограничить возможные T
классами, которые Comparable<T>
Вместо <T> пишем
…<T extends Comparable<T>> …
Можно ограничить T несколькими
типами (через &):
<T extends Comparable & Serializable>
и т.п.
Software Engineering
10

11. Множественные верхние границы

• Нам надо сортировать список из Number, но Number – не реализует
Comparable…
• Мы можем написать:
public <T extends Number & Comparable<T>> void sortNumbers( List<T> n ) {
Collections.sort( n );
}
– Тут T должен наследоваться из Number и быть Comparable.
• Так как множественного наследования нет, можно в качестве границы
использовать только один класс, и он должен быть указан первым.
• Например, так писать нельзя (на втором месте должен быть не класс, а
интерфейс):
<T extends Comparable<T> & Number>
19-Dec-22
Software Engineering
11

12. Generic-типы и наследование


Variable<Number> numberVariable = new Variable<>();
numberVariable.setValue(3.14); //ok, так как Double extends Number
numberVariable.setValue(3F); // ok, так как Float extends Number
numberVariable.setValue(3); // ok, так как Integer extends Number
• Пусть есть метод:
void someMethod(Variable<Number> numberVariable) {…}
• Какие типы аргумента он принимает?
– Он НЕ принимает Variable<Double>, Variable<Float>? Variable<Integer>, так
как они НЕ являются подтипами Variable<Number>…
• Из того, что Float - это подкласс Number, никак не следует, что
Variable<Float> - это подкласс Variable<Number>. У них есть лишь
общий суперкласс – Object (и больше – ничего общего…).
19-Dec-22
Software Engineering
12

13. Правила наследования. Примеры

19-Dec-22
Software Engineering
13

14. Наследование из generic класса

• При наследовании generic-класса можно подставить тип-параметр в < >
или оставить его как есть:
class Email extends Variable<String> {} //подставлен параметр String для T
class Age extends Variable<Integer> {} //подставлен параметр integer для T
class Height<T> extends Variable<T> {} // T вообще не изменяется
class ObjVariable extends Variable {} //не generic классы (raw type warning)
• Можно использовать более одного параметра-типа:
class MultiParamVariable<T, S> extends Variable<T> {
private S secondValue;
MultiParamVariable(T first, S second) {
super(first); secondValue = second;
}
}
19-Dec-22
Software Engineering
14

15. Type inference (выводимость типа)

• Это способность Java-компилятора анализировать каждый вызов
метода и его декларацию для определения типа (ов) аргумента (ов),
чтобы обеспечить вызов. Алгоритм выведения определяет типы
аргументов и, при необходимости, тип возвращаемого значения, при
этом стараясь найти наиболее специфический тип, который работает
со всеми аргументами.
• Например, есть generic-метод:
static <T> T pick(T a1, T a2) { return a2; }
и имеется его вызов:
Serializable s = pick("d", new ArrayList<String>());
– В декларации метода pick() есть один параметр-тип T, ищется такой самый
специфический тип, который является общим супертипом для String и для
ArrayList<String> и согласуется с типом возвращаемого значения – Serializable
(Cloneable – не годится, так как String не является Cloneable).
19-Dec-22
Software Engineering
15

16. Подстановочные типы

• Подстановочный тип (“джокер”) обозначается знаком ‘?’; при этом он
может иметь или не иметь верхнюю или нижнюю границу.
• Неограниченный подстановочный тип (Unbound Wildcard Type)
– Обозначается знаком <?>. Например: List<?> или Collection<?>…
– Он полезен, но имеет ограничения: из него можно брать объекты (типа Object),
но в него ничего нельзя класть, кроме значения null (и это не очень полезно);
– Например, пусть есть список «из неизвестно чего»:
List<?> stuff = … // new ArrayList<>() или может быть передан как параметр;
// stuff.add("abc");
// compile errors…
// stuff.add(new Object());
// stuff.add(3);
int numElements = stuff.size(); // ok…
– Он служит основой для ограниченных подстановочных типов;
– В List<Object> можно положить любой объект, а в List<?> - только null.
19-Dec-22
Software Engineering
16

17. Непривычный новый синтаксис

• В новых JDK есть, мягко говоря, сложно читаемые спецификации
методов, например: в классе java.util.stream.Collectors есть метод
static <T,K,D,A,M extends Map<K, D>> Collector<T,?,M> groupingBy(
Function<? super T,? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T,A,D> downstream){…}
• Чтобы их использовать правильно, надо понимать, что в них
написано.
• При использовании контейнеров (т.е. хранилищ элементов данных)
надо указывать тип содержащихся там элементов:
List<String> = new ArrayList<>();
Set<Employee> = new HashSet<>();
Variable<Integer> = new Variable<>(0);
19-Dec-22
Software Engineering
17

18.

• Указание типа элемента данных в контейнере преследует две цели:
– нельзя случайно туда засунуть не тот тип данных, что хотелось;
– не надо делать приведение (cast) к нужному типу при доставании из контейнера.
• Первое – бывает полезно редко, второе – весьма часто.
• В методах интерфейса List<E> параметр-тип E используется как (см.):





тип аргумента: add(E element);
тип возвращаемого значения: E get(int index);
ограниченная подстановка: addAll(Collection<? extends E> c);
никак не используется: void clear();
неизвестный тип.
• Следующий обобщенный (generic) метод читать уже труднее:
static <T extends Object & Comparable<? super T>> T min(
Collection<? extends T> collection
) // выдается Comparable Object, где Comparable определен для T и всех его предков,
а принимается любая коллекция с элементами типа T или его потомков…
19-Dec-22
Software Engineering
18

19. Синтаксический «дискомфорт»

• Все, что связано с wildcards (?), может сперва вызывать «удивление»…
– Удивляет уже то, что ArrayList<String> никак не связан с ArrayList<Object>.
– К списку объектов можно добавить (и взять из него) любой объект. Но к списку строк
нельзя добавить любой объект(!). К списку рыб можно добавить селедку, но к списку
селедок нельзя добавить любую рыбу (карася, к примеру).
• Параметризованные типа инвариантны: при декларации переменной
параметризованного типа в нее могут попасть только значения этого
самого типа (и никакие другие):
– Иначе можно было бы переложить список одного типа в переменную типа список
другого типа, добавить к нему что-то отличное от первого, пользуясь второй ссылкой
на список, а потом достать из первого списка добавленную вторым способом чуждую
ему ерунду…
• Однако, хочется уметь добавить в список (контейнер) чисел всякие
целые, дробные и т.п. числа (т.е. разновидности чисел разных типов).
Вот тут-то и появляются ограничения типа с wildcards…
19-Dec-22
Software Engineering
19

20. Wildcards (подстановочные знаки, «джокеры»)

• Wildcard – это тИповый аргумент, который обозначается с
использованием знака ‘?’ в угловых скобках.
• Как мы уже видели, Unbounded wildcard – полезен, но не сильно:
Если есть List<?> myList = new ArrayList<>(); // заносить в список нельзя;
int length = myList.length(); // можно брать/читать Object’ы
• Если такой параметризованный тип использовать при описании
аргумента метода, он сможет принять фактически любой список (по
которому можно пройтись итератором) :
private static void processList(List<?> list) {
System.out.println(list); // например…
}
Реализация такого метода может использовать методы самого списка (коллекции),
получать iterator и применять все методы класса Object (возможно, переопределенные) к
извлекаемым из списка элементам - объектам…
19-Dec-22
Software Engineering
20

21. Bounded Wildcards (тут начинается «веселье»…)

• Upper Bounded Wildcards используют ключевое слово extends для
указания ограничивающего суперкласса (и/или интерфейса). При этом,
чтобы определить список, где могут быть числа Integer, Long, Double и
другие наследники Number, мы можем написать:
List<? extends Number> numbers = new ArrayList<>();
• Но мы опять не можем туда добавлять значения!
– Компилятор не знает, что конкретно мы оттуда достанем; но знает, что Number’ы…
– Однако, мы снова можем принять такой список аргументом метода, в котором
сможем применять к элементам методы класса Number (например, метод
doubleValue()).
double sumList(List<? extends Number> list) {
double s = 0D; for(Number n : list) s += n.doubleValue();
return s;
} //При доступе к элементу мы (и компилятор) знаем, что он уж точно Number…
19-Dec-22
Software Engineering
21

22.

• Lower Bounded Wildcards – используют ключевое слово super и
показывают, что годится любой предок данного класса (по графу
наследования). Так, при записи List<? super Number> ссылка может
представлять значение, например, List<Number> или List<Object>.
• Если раньше (при Upper Bounded) мы добывали значения, чтобы к ним
применить имеющиеся у них методы, то теперь (при Lower Bounded) мы
хотим поставлять значения и быть уверенными, что это возможно (так
как внутри мы сможем принять значение в любой его «супертип»).
• Использование ограниченного снизу подстановочного типа означает,
что мы знаем, что список будет содержать элементы типа заданной
нижней границы, но мы можем использовать для их приема элементы
любого его супертипа.
• С Upper Bounded «джокером» мы извлекали нужные нам значения, с
Lower Bounded «джокером» мы поставляем значения в контейнер.
– Поясним на примере (см. следующий слайд…)
19-Dec-22
Software Engineering
22

23. Надо как-то «ограничить» используемые типы…

• Нижняя граница множества (Lower Bound) – значение, которое
меньше или равно каждому элементу множества данных
• Верхняя граница множества (Upper Bound) – значение, которое
больше или равно каждому элементу множества данных
• Например:
– для множества {3, 5, 11, 20, 22} 3 – lower bound, а 22 – upper bound. Но:
– 2 тоже lower bound (как и любое число, меньшее всех элементов множества)
– Аналогично, 50 или любое число, равное 22 или большее – это upper bound.
19-Dec-22
Software Engineering
23

24. Пример графа наследования (пусть есть указанный справа DAG)

• Рассмотрим extends:
List<? extends C2> list;
list может хранить ссылку на реализацию
какого-то списка (например, ArrayList), чей
параметризованной тип – один из 7-ми
подтипов С2 (С2 - включительно):
list = new ArrayList<C2>();
list = new ArrayList<D1>();
list = new ArrayList<D2>();
list = new ArrayList<E1>();
list = new ArrayList<E2>();
list = new ArrayList<E3>();
list = new ArrayList<E4>();
19-Dec-22
Software Engineering
24

25.

При этом у нас есть 7 разных возможных случаев (указанных и нарисованных ниже):
1). new ArrayList<C2>(): может хранить C2 D1 D2 E1 E2 E3 E4
2). new ArrayList<D1>(): может хранить
D1
E1 E2
3). new ArrayList<D2>(): может хранить
D2
E3 E4
4). new ArrayList<E1>(): может хранить
E1
5). new ArrayList<E2>(): может хранить
E2
6). new ArrayList<E3>(): может хранить
E3
7). new ArrayList<E4>(): может хранить
E4
19-Dec-22
Software Engineering
25

26.


Мы не можем написать следующее (по указанным рядом причинам):




нельзя писать list.add(new C2() {…}); // list мог быть list = new ArrayList<D1>(); …
нельзя писать list.add(new D1() {…}); // list мог быть list = new ArrayList<D2>(); …
и т.д. (так как список может оказаться списком с другим типом элементов…); мы не знаем,
с каким именно из семи возможных вариантов мы имеем дело;
Но мы можем добавить null (он есть в допустимых значениях любого списка).
• Рассмотрим Super:
List<? super C2> list;
тут list может хранить ссылку на реализацию какого-то списка (например,
ArrayList), чей параметризованный тип – один из 7-ми «надтипов» (supertypes of)
С2 (С2 - включительно):




new ArrayList<A1>(); // объект, способный хранить A1 или подтипы; или
new ArrayList<A2>(); // объект, способный хранить A2 или подтипы; или
new ArrayList<A3>(); // объект, способный хранить A3 или подтипы; или
new ArrayList<A3>(); // объект, способный хранить A4 или подтипы; или …
19-Dec-22
Software Engineering
26

27.


Мы имеем 7 различных случаев (и множества хранимых при них типов):
1). new ArrayList<A1>(): м.б. хранит A1
B1 B2
C1 C2
D1 D2 E1 E2 E3 E4
2). new ArrayList<A2>(): м.б. хранит
A2
B2
C1 C2
D1 D2 E1 E2 E3 E4
3). new ArrayList<A3>(): м.б. хранит
A3
B3
C2 C3 D1 D2 E1 E2 E3 E4
4). new ArrayList<A4>(): м.б. хранит
A4
B3 B4
С2 C3 D1 D2 E1 E2 E3 E4
5). new ArrayList<B2>(): м.б. хранит
B2
C1 C2
D1 D2 E1 E2 E3 E4
6). new ArrayList<B3>(): м.б. хранит
B3
C2 C3 D1 D2 E1 E2 E3 E4
7). new ArrayList<C2>(): м.б. хранит
C2
D1 D2 E1 E2 E3 E4
19-Dec-22
Software Engineering
27

28.


Тут есть семь безопасных типов, которые являются общими для всех случаев:
С2, D1, D2, E1, E2, E3, E4.
– Допустимо list.add(new C2() {…}); //при любом типе списка, C2 разрешается;
– Допустимо list.add(new D1() {…}); //при любом типе списка, D1 разрешается;
– и т.д,
Как можно заметить, эти типы соответствуют иерархии, начиная с С2.
• Правила, связанные с использованием wildcards («джокеров»):
– добавление элемента в список:
• List<? extends X> - запрещено добавлять что-либо, кроме null.
• List<? super X> - можно добавлять то, что is-a X (X или его subtype), или null.
– в любой (mutable) контейнер можно положить null (допустимое ссылочным типом).
– получение элемента из списка (есть там вообще кто-то есть):
• List<? extends X> - можно принять в переменную типа X или его супертипа.
• List<? super X> - можно принять только в переменную типа Object.
– из любого контейнера можно принимать значение в переменную типа Object.
19-Dec-22
Software Engineering
28

29. Ограниченные подстановочные типы

• Подстстановочные типы с вехней границей (Upper Bounded
Wildcards) обозначаются с указанием предельного супертипа
(суперкласса или интерфейса); сам супертип – тоже годится.
– Например: List <? extends Number> list = new ArrayList<>();
– Класть туда по-прежнему нельзя (ничего, кроме null);
– Но это гарантирует, что мы достанем оттуда объект типа Number, если
нам список такого типа передадут (сделав его как-то иначе...);
• Подстановочные типы с нижней границей (Lower Bounded
Wildcards) говорят, что годится любой предок типа, указанного за
super (включая его самого).
– Так: List<? super Integer> допускает Integer, Number, Object, что еще?
– Мы хотим класть Integers(s) в любой список, куда их можно положить;
а их можно класть в списки с элементами типов-предков Integer…
19-Dec-22
Software Engineering
29

30. PECS (Producer Extends, Consumer Super) – Буратино в стране дураков?

• Это – мнемоника из книги Дж. Блоха (Какой? Кто из вас ее читает / открыл?).
– Если параметризованный тип представляет поставщика данных (producer’а),
надо использовать extends; (т.е. оттуда берут данные…)
– Если параметризованный тип представляет потребителя данных
(consumer’а), надо использовать super; (т.е. туда кладут данные…)
– Если параметр подразумевает и то, и другое, - надо использовать явный
тип.
• Или (иначе говоря) используйте:
– extends, если только добываете данные из структуры (read access, ввод);
– super, если только кладете туда данные (write access, вывод);
– параметр-тип явно; если параметризованный тип является и тем, и другим
(и поставщиком, и потребителем), то не надо использовать «джокера» (с ?)
вообще – надо писать сам параметр-тип явно, так как он единственный, кто
при этом годится.
19-Dec-22
Software Engineering
30

31. Терминология (встречается в Java, Scala и др.)

Способы переноса наследования типов на производные от них типы:
• Ковариантность - сохранение иерархии наследования исходных типов в
производных от них типах в том же порядке.
– массивы в Java ковариантны: Herring[ ] is a Fish[ ]: «бочка с селедками» – это
частный случай «бочки рыбы», так как селедка - это рыба, Контейнер-массив T[ ]
ковариантен своему параметру-типу T.Но коллекции в Java не являются
ковариантными, если только мы не используем <? extends … >.
Контравариантность - иерархии исходных типов меняются на
противоположные в производных типах.
– если «все селедки — рыбы», то «всякий метод, оперирующий произвольными
рыбами, может выполнить операцию над селедкой», но не наоборот. В Java для
этого есть <? super … >
Инвариантность – отсутствие наследования между производными типами.
– Обычные обобщенные типы в Java инвариантны, но для wildcard-типов получается,
что List<? extends Animal> будет ковариантен подставляемому типу, а List<? super
Animal> — контравариантен.
19-Dec-22
Software Engineering
31

32. Терминология

• Имеется соответствующая терминология, используемая в других
языках (в Scala, в частности):
• Ковариантность – упорядочение типов от более конкретного к более
общему.
– В Java массивы являются ковариантными: String[] является подтипом Object[]. Как мы
видели, коллекции в Java не являются ковариантными, только если мы не
используем Upper Bounded Wildcards (т.е. <? extends …>).
• Контравариантность – упорядочение в обратную сторону.
– В Java для этого используются Lower Bounded Wildcards (то есть <? super …>).
• Инвариантность – означает, что тип должен быть точно такой, как
указано.
– Все параметризованные типы в Java являются инвариантными, если мы не
пользуемся wildcards c extends или super; если метод предполагает List<Employee>,
то именно его и надо предоставить (ни List<Object>, ни List<Salaried> не годятся).
19-Dec-22
Software Engineering
32

33. Пример наследования: Upper Bounded Wildcard types


Manager extends Employee
Variable<? extends Employee> это любой generic тип Variable,
параметр которого – подкласс
Employee (включительно).
<? super Manager> – этот
подстановочный тип ограничен
всеми супертипами Manager
(включая его самого)
Variable<?> - это «переменная не
знаю чего...»
Variable – raw type (голый тип от
generic-типа Variable<T>)
Как дела с селедками и рыбами?
19-Dec-22
Software Engineering
33

34.

Collection<?> - это супертип всех коллекций (коллекций, чей тип
совпадает с чем угодно...); это “unbounded wildcard”
– Тогда можем использовать это для печати любой коллекции:
void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
• Bounded wildcard:
? extends Shape – это тип Shape или его наследник (кто именно
– неизвестно, известно только, что можно взять значение этого типа и
присвоить в Shape-переменную).
? super Shape – это тип Shape или его супертип (кто именно –
неизвестно...), но можно дать туда значение из Shape-переменной...
19-Dec-22
Software Engineering
34

35. Универсальный код и Java VM

• Java VM ничего не знает про эти ухищрения компилятора.
• После компиляции получается так, что универсальный тип
преобразуется в «голый» (raw) тип (его имя получается
отбрасыванием параметров типа), переменные типа заменяются на
ограничивающий тип (или Object) - и все преобразуется в то, что было
до JDK 5.0.
– Мы с вами до сих пор программировали с raw-типами («голыми»…)
– Процесс отбрасывания параметров типа называется type erasure (стирание типа)
• При замене на ограничивающий тип берут первый их них, если их
несколько.
– Например, для <T extends Comparable & Serializable> возьмут Comparable.
Поэтому, при перечислении лучше ставить в конец списка интерфейсы с реже
вызываемыми методами (так как к ним делается cast…)
19-Dec-22
Software Engineering
35

36. Преобразование выражений

• Когда в программе вызывается параметризованный метод, компилятор
автоматически вставляет cast для приведения типа возвращаемого
значения (см. пример слайда 7 и использование Variable<String> с
декомпилятором, в IDE Idea он есть, или через ASM Plugin или Bytecode
Viewer Plugin для Idea…):
Variable<String> vs = … // например, new Variable<>(); см. слайд 7…
String s = vs.getValue(); // компилятор сделает (String) vs.getValue()
• Когда в программе обращаются непосредственно к полю, которое в
raw-виде оказалось с другим raw-типом, то тоже вставится cast:
String s = (String) vs.value; // это будет видно, если убрать private перед value (см. слайд 7)
19-Dec-22
Software Engineering
36

37. Преобразование методов

• Из объявления, например
public static <T extends Comparable<T>> minimum (T [ ] array)
получается
public static Comparable minimum (Comparable [ ] array)
• Имеются тонкости и проблемы, связанные с наследованием
(overriding) при «стирании» информации о типах...
• Они решаются генерацией специальных дополнительных методов
(иногда их называют bridge-methods; мы посмотрим пример на
семинаре...);
– такие методы являются «синтетическими» - их генерирует сам компилятор;
– попробуйте придумать такие примеры самостоятельно…
19-Dec-22
Software Engineering
37

38. Надо помнить, что:

• Виртуальные машины (JVMs) ничего не знают про generics – там
работают «обычные» классы и методы;
• Все параметры-типы заменяются их границами при компиляции;
• Для сохранения полиморфизма компилятором вставляются
синтетические методы (если надо);
• Для поддержки правильности типов там, где надо, вставляются
cast’ы.
19-Dec-22
Software Engineering
38

39. Ограничения при работе с generic типами

• Эти ограничения (в большинстве) связаны со стиранием типа;
• Параметры - типы не могут быть примитивными типами;
– Нельзя написать Variable<double> (его не свести к Object…)
• Проверка типов в динамике работает только с raw (голыми)
типами;
– If ( а instanceof Variable<String>)
– getClass() и т.п. … ничего не знают про generics (работают с raw-типами)
• Нельзя возбуждать и ловить исключения generic-типа
– Нельзя наследовать универсальный класс от Throwable
– Нельзя использовать переменную-тип в выражении catch()
– НО: в выражении throws – можно!
public static <T extends Throwable> void doWork(T t) throws T
19-Dec-22
Software Engineering
{…}
39

40.

• Запрещены создания массивов параметризованных типов
– Нельзя писать Variable<String>[ ] vsa = new Variable<String>[5];
• Инстанциировать типы-переменные нельзя (не ясно, кто это…)
– Нельзя писать new T(…), new T[…], T.class (тоже нельзя…)
• Типы-переменные не допускаются в статическом контексте
универсальных типов
– Разнотипные Singleton’ы при переходе к raw-типу «совпадают»...
• Необходимо следить за возможными конфликтами из-за
«стирания» типа (type erasure)... Быть может – из-за этого понадобится
переименовывать методы...
• Надо соблюдать правило: класс или переменная типа не может
одновременно быть подтипом двух интерфейсов, которые сводятся
к одному интерфейсу (являются двумя параметризациями одного
интерфейса).
19-Dec-22
Software Engineering
40

41. Такие классы НЕ могут быть generic:

• Анонимные вложенные классы
– У них нет имени, поэтому их нельзя инстанциировать, передавая
фактические параметры типа...
– Но они могут реализовывать параметризованный интерфейс или
наследовать параметризованный класс
• Enum’ы – они по сути static, а параметры типа нельзя использовать в
статическом контексте. (Мы обсудим Enum’ы позже…)
• Exception – это runtime-механизм, а runtime – не знает про generics…
• Все остальные классы и интерфейсы – могут быть generic !
19-Dec-22
Software Engineering
41

42. Рекомендации по использованию (Дж.Блох)

• Использование raw-типов может приводить к exceptions at runtime. Надо
избегать их использования – они нужны только для совместимости со
старым кодом (его – много, и надо понимать, что речь идет только о статическом
контроле типов, которого, например, в Python’е – просто нет...);
• Надежное программирование (есть параметризация типов):
– Set<Object> - множество, содержащее объекты любого типа;
– Set<?> - множество, содержащее объекты какого-то неизвестного типа;
• Ненадежное программирование (нет параметризации типов):
– Set – множество, про которое система [параметризованных] типов ничего не знает.
• При работе с generics нужно особое внимание к Warnings компилятора
– Не надо «маскировать проблемы» - это «страусиное поведение»;
– Не надо использовать аннотации @SuppressWarnings(“unchecked”) без полной
(доказуемой) уверенности в надежности кода...
19-Dec-22
Software Engineering
42

43. Аннотация @SuppressWarnings(“unchecked”)

• Может использоваться при любой декларации – от локальной
переменной до всего класса. Ими надо пользоваться с минимальной
областью действия (при одной переменной или коротком методе).
В книге Effective Java (2018) приведен пример из ArrayList<E>-класса:
@SuppressWarnings(“unchecked”) public <T> T[ ] toArray(T[ ] a) {…} // см. JDK...
автор этого класса Josh Bloch - в книге пишет, что так делать не надо (Item 27)!
• “unchecked” warning – предупреждение от комилятора о том, что он не
может гарантировать безопасность типов (type safe): «не проверено...»
• Чаще всего это происходит при использовании raw-типов, когда не
хватает информации для проверки (что может дать Exception в runtime).
http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ001
• Подробное обсуждение см. в Java Generics FAQs by Angelica Langer
(можно будет взять в SmartLMS)…
19-Dec-22
Software Engineering
43

44. Расширение средств reflection для generics

• Как ни странно, at runtime можно (несмотря на type erasure) получить
некоторую информацию про generic-типы с помощью reflection API.
• Класс Class – является generic классом (см. метод: T cast() и др.).
• Есть API в reflection, позволяющий кое-что узнать (см.) про genericтипы...
– Можно реконструировать про generic-классы и методы все, что было
декларировано их создателем (это видно при декомпиляции);
– Для этого используется интерфейс java.lang.reflect.Type и его
подтипы.
19-Dec-22
Software Engineering
44

45. Заключение

Литература (все есть в SmartLMS)
Хорстманн, CoreJava, т.1, глава 8.
Effective Java (см. гл.5);
Herbert Schildt, Java. The complete Reference, Ed.11, chapter 14.
Дополнительно (в том числе – в SmartLMS, папка Generics):
https://en.wikipedia.org/wiki/Generics_in_Java#cite_note-2

Nada Amin, Ross Tate, Java and Scala’s Type Systems are Unsound (SmatLMS, Readings, Generics)
https://docs.oracle.com/javase/tutorial/java/generics/index.html
Tutorial Gilad Bracha: https://docs.oracle.com/javase/tutorial/java/generics/types.html
Angelika Langer. Java Generics FAQs
Глава 21 - в книге Java Notes for Professionals.
Appendix A - в книге Modern Java Recipes.
Brian Goetz, Обобщенные типы (англ., есть русский перевод первой части)
Брюс Эккель, Философия Java (Thinking in Java), глава 15.
19-Dec-22
Software Engineering
46
English     Русский Rules