Дженерики (Generics) в java
Начиная с JDK 1.5, в Java появились новые возможности для программирования. Одним из таких нововведений являются Generics. Generics являются аналогией с конструкцией “Шаблонов”(template) в С++, но имеет свои нюансы. Generics позволяют абстрагировать множество типов. Наиболее распространенными примерами являются Коллекции.
Вот типичное использование такого рода (без Generics):
1. List myIntList = new LinkedList(); 2. myIntList.add(new Integer(0)); 3. Integer x = (Integer) myIntList.iterator().next();
Как правило, программист знает, какие данные должны быть в List’e. Тем не менее, стоит обратить особое внимание на Приведение типа (“Cast”) в строчке 3. Компилятор может лишь гарантировать, что метод next() вернёт Object, но чтобы обеспечить присвоение переменной типа Integer правильным и безопасным, требуется Cast. Cast не только создает беспорядки, но дает возможность появление ошибки “Runtime Error” из-за невнимательности программиста.
И появляется такой вопрос: “Как с этим бороться? ” В частности: “Как же зарезервировать List для определенного типа данных?”
Как раз такую проблему решают Generics.
1. List myIntList = new LinkedList (); 2. myIntList.add(new Integer(0)); 3. Integer x = myIntList.iterator().next();
Обратите внимание на объявления типа для переменной myIntList. Он указывает на то, что это не просто произвольный List, а List. Мы говорим, что List является generic-интерфейсом, который принимает параметр типа – в этом случае, Integer. Кроме того, необходимо обратить внимание на то, что теперь Cast выполняется в строчке 3 автоматически.
Некоторые могут задуматься, что беспорядок в коде увеличился, но это не так. Вместо приведения к Integer в строчке 3, у нас теперь есть Integer в качестве параметра в строчке 1. Здесь существенное отличие. Теперь компилятор может проверить этот тип на корректность во время компиляции.
И когда мы говорим, что myIntList объявлен как List, это будет справедливо во всем коде и компилятор это гарантирует.
Эффект от Generics особенно проявляется в крупных проектах: он улучшает читаемость и надежность кода в целом.
- Свойства
- Пример реализации Generic-класса
- Несовместимость generic-типов
- Проблемы реализации Generics
- Ограничения Generic
- Преобразование типов
- Примеры кода
Свойства
- Строгая типизация
- Единая реализация
- Отсутствие информации о типе
Пример реализации Generic-класса
public interface List < E get(int i); set(int i, E e); add(E e); Iteratoriterator(); … >
Для того чтобы использовать класс как Generics, мы должны прописать после имени класса , куда можно подставить любое имя, wildcard и т.д.
После того как было объявлено имя generic-типа его можно использовать как обычный тип внутри метода. И когда в коде будет объявлен, к примеру, List, то Е станет Integer для переменной list (как показано ниже).
Теперь рассмотрим чем старая реализация кода отличается от новой:
List ─ список элементов E
List list = new List(); list.add(new Integer(1)); Integer i = (Integer) list.get(0);
List list = new List(); list.add(new Integer(1)); Integer i = list.get(0);
Как видите, больше не нужно приводить Integer, так как метод get() возвращает ссылку на объект конкретного типа (в данном случае – Integer).
Несовместимость generic-типов
Это одна из самых важных вещей, которую вы должны узнать о Generics
Как говориться: “В бочке мёда есть ложка дегтя”. Для того чтобы сохранить целостности и независимости друг от друга Коллекции, у Generics существует так называемая “Несовместимость generic-типов”.
Пусть у нас есть тип Foo, который является подтипом Bar, и еще G - наследник Коллекции. То G не является наследником G.
List li = new ArrayList(); List lo = li;
lo.add(“hello”); // ClassCastException: String -> int Integer li = lo.get(0);
Проблемы реализации Generics
- Решение 1 – Wildcard
Пусть мы захотели написать метод, который берет Collection и выводит на экран. И мы захотели вызвать dump для Integer.
void dump(Collection c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Object o = i.next(); System.out.println(o); >>
List l; dump(l); List l; dump(l); // Ошибка
В этом примере List не может использовать метод dump, так как он не является подтипом List.
Проблема в том что эта реализация кода не эффективна, так как Collection не является полностью родительской коллекцией всех остальных коллекции, грубо говоря Collection имеет ограничения.
Для решения этой проблемы используется Wildcard (“?”). Он не имеет ограничения в использовании(то есть имеет соответствие с любым типом) и в этом его плюсы. И теперь, мы можем назвать это с любым типом коллекции.
void dump(Collection c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Object o = i.next(); System.out.println(o); >>
- Решение 2 – Bounded Wildcard
Пусть мы захотели написать метод, который рисует List. И у Shape есть наследник Circle. И мы хотим вызвать draw для Circle.
void draw(List c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Shape s = i.next(); s.draw(); >>
List l; draw(l); List l; draw(l); // Ошибка
Проблема в том, что у нас не получиться из-за несовместимости типов. Предложенное решение используется, если метод который нужно реализовать использовал бы определенный тип и его подтипов. Так называемое “Ограничение сверху”. Для этого нужно вместо прописать .
void draw(List c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Shape s = i.next(); s.draw(); >>
- Решение 3 – Generic-Метод
Пусть вы захотели сделать метод, который берет массив Object и переносить их в коллекцию.
void addAll(Object[] a, Collection c) < for (int i = 0; i < a.length; i++) < c.add(a[i]); >>
addAll(new String[10], new ArrayList()); addAll(new Object[10], new ArrayList()); addAll(new Object[10], new ArrayList()); // Ошибка addAll(new String[10], new ArrayList()); // Ошибка
Напомним, что вы не можете просто засунуть Object в коллекции неизвестного типа. Способ решения этой проблемы является использование “Generic-Метод” Для этого перед методом нужно объявить и использовать его.
void addAll(T[] a, Collection c) < for (int i = 0; i < a.length; i++) < c.add(a[i]); >>
Но все равно после выполнение останется ошибка в третьей строчке :
addAll(new Object[10], new ArrayList()); // Ошибка
- Решение 4 – Bounded type argument
Реализуем метод копирование из одной коллекции в другую
void addAll(Collection c, Collection c2) < for (Iteratori = c.iterator(); i.hasNext(); ) < M o = i.next(); c2.add(o); >>
addAll(new AL(), new AL()); addAll(new AL(), new AL()); //Ошибка
Проблема в том что две Коллекции могут быть разных типов (несовместимость generic-типов). Для таких случаев было придуман Bounded type argument. Он нужен если метод ,который мы пишем использовал бы определенный тип данных. Для этого нужно ввести (N принимает только значения M). Также можно корректно писать . (Принимает значения нескольких переменных)
void addAll(Collection c, Collection c2) < for (Iteratori = c.iterator(); i.hasNext(); ) < N o = i.next(); c2.add(o); >>
- Решение 5 – Lower bounded wcard
Реализуем метод нахождение максимума в коллекции.
> T max(Collection c)
List il; Integer I = max(il); class Test implements Comparable List tl; Test t = max(tl); // Ошибка
- > обозначает что Т обязан реализовывать интерфейс Comparable.
Ошибка возникает из за того что Test реализует интерфейс Comparable. Решение этой проблемы – Lower bounded wcard(“Ограничение снизу”). Суть в том что мы будет реализовывать метод не только для Т, но и для его Супер-типов(Родительских типов). Например: Если мы напишем
List list;
Мы можем заполнить его List, List или List.
> T max(Collection c)
- Решение 6 – Wildcard Capture
Реализуем метод Swap в List
void swap(List list, int i, int j) < list.set(i, list.get(j)); // Ошибка >
Проблема в том, что метод List.set() не может работать с List, так как ему не известно как он List. Для решение этой проблемы используют “Wildcard Capture” (или “Capture helpers”). Суть заключается в том, чтобы обмануть компилятор. Напишем еще один метод с параметризованной переменной и будем его использовать внутри нашего метода.
void swap(List list, int i, int j) < swapImpl(list, i, j); > void swapImpl(List list, int i, int j)
Ограничения Generic
Также нужно запомнить простые правила для работы с Generics.
- Невозможно создать массив параметра типа
Collection c; T[] ta; new T[10]; // Ошибка !!
- Невозможно создать массив Generic-классов
new ArrayList>(); List[] la = new List[10]; // Ошибка !!
Преобразование типов
В Generics также можно манипулировать с информацией, хранящийся в переменных.
- Уничтожение информации о типе
List l = new ArrayList();
- Добавление информации о типе
List l = (List) new ArrayList(); List l1 = new ArrayList();
Примеры кода
- Первый пример:
List ls; List li; ls.getClass() == li.getClass() // True ls instanceof List // True ls instanceof List // Запрещено
- Второй пример:
Нахождение максимума в Коллекции Integer.
Collection c; Iterator i = c.iterator(); Integer max = (Integer) i.next(); while(i.hasNext()) < Integer next = (Integer) i.next(); if (next.compareTo(max) > 0) < max = next; >>
- С помощью Generics
Collection c; Iterator i = c.iterator(); Integer max = i.next(); while(i.hasNext()) < Integer next = i.next(); if (next.compareTo(max) >0) < max = next; >>
Вам також може сподобатися

Уроки по android разработке на Java 1 9 845
На этом уроке рассмотрим , как обновлять и удалять строки в базе данных SQLite

Инструменты android разработчика 0 8 237
Перевод статьи Laurence Moroney, Developer Advocate, ссылка на оригинал В релизе выпуска Google Play

Уроки по android разработке на Java 0 876
Этот урок о том, как создать Android-приложение галерею или слайдер с использованием виджета ViewPager,

Создаем android-приложения 10 4 648
В этом уроке разберем код приложения, которое записывает видео с экрана устройства со звуком.
![]()
Дизайн android приложений 2 6 618
Библиотека поддержки Android Design Support Library облегчает разработку, обеспечивая обратную совместимость множества компонентов в материальном стиле, вплоть

Архитектура андроид-приложений 5 7 157
Вторая лекция курса по архитектуре клиент-серверных android-приложений, в которой мы рассмотрим такие понятия, как REST-архитектура, ContentProvider,
Что такое дженерик java
«Назначив для него при создании тип данных (
Andrey Karelin Уровень 41
30 апреля 2022
Вот сколько читаю про дженерики, нигде не проговаривается очевидная вещь, которая помогает понять их суть, а именно то, что использование типа T, K, V и т.п. обозначает лишь условный тип класса. И главное тут не название, а то какие типы методов/переменных объекта будут возвращаться/работать, по отношению к определенному изначально типу. Например, указанный в примере метод public static void fill(List list, T val) говорит нам о том, что в метод мы можем передать list и val только одинакового между собой типа, и такого же типа, которым при создании был определен класс.
Глеб Уровень 29 Expert
21 апреля 2022
Полезная статья
Q1R27 Уровень 17
28 марта 2022
Lex Bekker Уровень 12
8 марта 2022
LuneFox Уровень 41 Expert
26 января 2022
Добавлю ещё, кстати, что писать именно T не обязательно. Как я понимаю, это конвенция от слова Type. Допускается написание любой лабуды в качестве маркера для дженерика, например:
public static KEK fill(List list, KEK value) < for (int i = 0; i < list.size(); i++) < list.set(i, value); >return list.get(0); >

При этом повторный код с другим названием этого типа ловится IDE-шкой. Именно поэтому перед объявлением возвращаемого значения не написать , то программа будет думать, что это не дженерик, а настоящий класс с названием T, и будет ругаться, что не знает такого класса или попросит, чтобы ты создал class T< >. Конечно, то, что я написал — наверняка очевидные вещи, но иногда полезно пощупать код руками и попытаться «сломать систему», чтобы в голове отложилась чёткая картинка, как делать можно, а как нельзя, и почему.
Пришел, увидел, обобщил: погружаемся в Java Generics
Java Generics — это одно из самых значительных изменений за всю историю языка Java. «Дженерики», доступные с Java 5, сделали использование Java Collection Framework проще, удобнее и безопаснее. Ошибки, связанные с некорректным использованием типов, теперь обнаруживаются на этапе компиляции. Да и сам язык Java стал еще безопаснее. Несмотря на кажущуюся простоту обобщенных типов, многие разработчики сталкиваются с трудностями при их использовании. В этом посте я расскажу об особенностях работы с Java Generics, чтобы этих трудностей у вас было поменьше. Пригодится, если вы не гуру в дженериках, и поможет избежать много трудностей при погружении в тему.
Работа с коллекциями
Предположим, банку нужно подсчитать сумму сбережений на счетах клиентов. До появления «дженериков» метод вычисления суммы выглядел так:
public long getSum(List accounts) < long sum = 0; for (int i = 0, n = accounts.size(); i < n; i++) < Object account = accounts.get(i); if (account instanceof Account) < sum += ((Account) account).getAmount(); >> return sum; >
Мы итерировались, пробегались по списку аккаунтов и проверяли, действительно ли элемент из этого списка является экземпляром класса Account — то есть счетом пользователя. Выполняли приведение типа нашего объекта класса Account и метод getAmount , который возвращал сумму на этом счете. Дальше все это суммировали и возвращали итоговую сумму. Требовалось выполнить два действия:
if (account instanceof Account) < // (1)
sum += ((Account) account).getAmount(); // (2)
Если не сделать проверку ( instanceof ) на принадлежность к классу Account , то на втором этапе возможен ClassCastException – то есть аварийное завершение программы. Поэтому такая проверка была обязательной.
С появлением Generics необходимость в проверке и приведении типа отпала:
public long getSum2(List accounts) < long sum = 0; for (Account account : accounts) < sum += account.getAmount(); >return sum; >
Теперь метод
getSum2(List accounts)
принимает в качестве аргументов только список объектов класса Account . Это ограничение указано в самом методе, в его сигнатуре, программист просто не может передать никакой другой список — только список клиентских счетов.
Нам не нужно выполнять проверку типа элементов из этого списка: она подразумевается описанием типа у параметра метода
List accounts
(можно прочитать как список объектов класса Account ). И компилятор выдаст ошибку, если что-то пойдет не так — то есть если кто-то попробует передать в этот метод список объектов, отличных от класса Account .
Во второй строчке проверки необходимость тоже отпадала. Если потребуется, приведение типов ( casting ) будет сделано на этапе компиляции.
Принцип подстановки
Принцип подстановки Барбары Лисков – специфичное определение подтипа в объектно-ориентированном программировании. Идея Лисков о «подтипе» дает определение понятия замещения: если S является подтипом T , тогда объекты типа T в программе могут быть замещены объектами типа S без каких-либо изменений желательных свойств этой программы.
| Тип | Подтип |
| Number | Integer |
| List | ArrayList |
| Collection | List |
| Iterable | Collection |
Примеры отношения тип/подтип
Вот пример использования принципа подстановки в Java:
Number n = Integer.valueOf(42); List aList = new ArrayList<>(); Collection aCollection = aList; Iterable iterable = aCollection;
Integer является подтипом Number , следовательно, переменной n типа Number можно присвоить значение, которое возвращает метод Integer.valueOf(42) .
Ковариантность, контравариантность и инвариантность
Сначала немного теории. Ковариантность — это сохранение иерархии наследования исходных типов в производных типах в том же порядке. Например, если Кошка — это подтип Животные, то Множество — это подтип Множество . Следовательно, с учетом принципа подстановки можно выполнить такое присваивание:
Множество = Множество
Контравариантность — это обращение иерархии исходных типов на противоположную в производных типах. Например, если Кошка — это подтип Животные , то Множество — это подтип Множество . Следовательно, с учетом принципа подстановки можно выполнить такое присваивание:
Множество = Множество
Инвариантность — отсутствие наследования между производными типами. Если Кошка — это подтип Животные, то Множество не является подтипом Множество и Множество не является подтипом Множество .
Массивы в Java ковариантны. Тип S[] является подтипом T[] , если S — подтип T . Пример присваивания:
String[] strings = new String[] ; Object[] arr = strings;
Мы присвоили ссылку на массив строк переменной arr , тип которой – «массив объектов» . Если бы массивы не были ковариантными, нам бы это сделать не удалось. Java позволяет это сделать, программа скомпилируется и выполнится без ошибок.
arr[0] = 42; // ArrayStoreException. Проблема обнаружилась на этапе выполнения программы
Но если мы попытаемся изменить содержимое массива через переменную arr и запишем туда число 42, то получим ArrayStoreException на этапе выполнения программы, поскольку 42 является не строкой, а числом. В этом недостаток ковариантности массивов Java: мы не можем выполнить проверки на этапе компиляции, и что-то может сломаться уже в рантайме.
«Дженерики» инвариантны. Приведем пример:
List ints = Arrays.asList(1,2,3); List nums = ints; // compile-time error. Проблема обнаружилась на этапе компиляции nums.set(2, 3.14); assert ints.toString().equals("[1, 2, 3.14]");
Если взять список целых чисел, то он не будет являться ни подтипом типа Number , ни каким-либо другим подтипом. Он является только подтипом самого себя. То есть List — это List и ничего больше. Компилятор позаботится о том, чтобы переменная ints , объявленная как список объектов класса Integer, содержала только объекты класса Integer и ничего кроме них. На этапе компиляции производится проверка, и у нас в рантайме уже ничего не упадет.
Wildcards
Всегда ли Generics инварианты? Нет. Приведу примеры:
List ints = new ArrayList(); List nums = ints;
Это ковариантность. List — подтип List
List nums = new ArrayList(); List ints = nums;
Это контравариантность. List является подтипом List .
Запись вида "? extends . " или "? super . " — называется wildcard или символом подстановки, с верхней границей ( extends ) или с нижней границей ( super ). List может содержать объекты, класс которых является Number или наследуется от Number . List может содержать объекты, класс которых Number или у которых Number является наследником (супертип от Number ).
Запись вида T2 1 означает, что набор типов описываемых T2 является подмножеством набора типов описываемых T1
Пара задачек для проверки знаний:
1. Почему в примере ниже compile-time error? Какое значение можно добавить в список nums ?
List ints = new ArrayList(); ints.add(1); ints.add(2); List nums = ints; nums.add(3.14); // compile-time error
Если контейнер объявлен с wildcard ? extends , то можно только читать значения. В список нельзя ничего добавить, кроме null . Для того чтобы добавить объект в список нам нужен другой тип wildcard — ? super
2. Почему нельзя получить элемент из списка ниже?
public static T getFirst(List list) < return list.get(0); // compile-time error >
Нельзя прочитать элемент из контейнера с wildcard ? super , кроме объекта класса Object
public static Object getFirst(List list)
The Get and Put Principle или PECS (Producer Extends Consumer Super)
Особенность wildcard с верхней и нижней границей дает дополнительные фичи, связанные с безопасным использованием типов. Из одного типа переменных можно только читать, в другой — только вписывать (исключением является возможность записать null для extends и прочитать Object для super ). Чтобы было легче запомнить, когда какой wildcard использовать, существует принцип PECS — Producer Extends Consumer Super.
- Если мы объявили wildcard с extends, то это producer. Он только «продюсирует», предоставляет элемент из контейнера, а сам ничего не принимает.
- Если же мы объявили wildcard с super — то это consumer. Он только принимает, а предоставить ничего не может.
public static void copy(List dest, List src)
Метод осуществляет копирование элементов из исходного списка src в список dest . src — объявлен с wildcard ? extends и является продюсером, а dest — объявлен с wildcard ? super и является потребителем. Учитывая ковариантность и контравариантность wildcard, можно скопировать элементы из списка ints в список nums :
List nums = Arrays.asList(4.1F, 0.2F); List ints = Arrays.asList(1,2); Collections.copy(nums, ints);
Если же мы по ошибке перепутаем параметры метода copy и попытаемся выполнить копирование из списка nums в список ints , то компилятор не позволит нам это сделать:
Collections.copy(ints, nums); // Compile-time error
и Raw типы
Ниже приведен wildcard с неограниченным символом подстановки. Мы просто ставим , без ключевых слов super или extends :
static void printCollection(Collection c) < // a wildcard collection for (Object o : c) < System.out.println(o); >>
На самом деле такой «неограниченный» wildcard все-таки ограничен, сверху. Collection — это тоже символ подстановки, как и " ? extends Object ". Запись вида Collection равносильна Collection , а значит — коллекция может содержать объекты любого класса, так как все классы в Java наследуются от Object – поэтому подстановка называется неограниченной.
Если мы опустим указание типа, например, как здесь:
ArrayList arrayList = new ArrayList();
то, говорят, что ArrayList — это Raw тип параметризованного ArrayList . Используя Raw типы, мы возвращаемся в эру до дженериков и сознательно отказываемся от всех фич, присущих параметризованным типам.
Если мы попытаемся вызвать параметризованный метода у Raw типа, то компилятор выдаст нам предупреждение «Unchecked call». Если мы попытаемся выполнить присваивание ссылки на параметризованный тип Raw типу, то компилятор выдаст предупреждение «Unchecked assignment». Игнорирование этих предупреждений, как мы увидим позже, может привести к ошибкам во время выполнения нашего приложения.
ArrayList strings = new ArrayList<>(); ArrayList arrayList = new ArrayList(); arrayList = strings; // Ok strings = arrayList; // Unchecked assignment arrayList.add(1); //unchecked call
Wildcard Capture
Попробуем теперь реализовать метод, выполняющий перестановку элементов списка в обратном порядке.
public static void reverse(List list); // Ошибка! public static void reverse(List list) < Listtmp = new ArrayList(list); for (int i = 0; i < list.size(); i++) < list.set(i, tmp.get(list.size()-i-1)); // compile-time error >>
Ошибка компиляции возникла, потому что в методе reverse в качестве аргумента принимается список с неограниченным символом подстановки .
означает то же что и . Следовательно, согласно принципу PECS, list – это producer . А producer только продюсирует элементы. А мы в цикле for вызываем метод set() , т.е. пытаемся записать в list . И поэтому упираемся в защиту Java, что не позволяет установить какое-то значение по индексу.
Что делать? Нам поможет паттерн Wildcard Capture . Здесь мы создаем обобщенный метод rev . Он объявлен с переменной типа T . Этот метод принимает список типов T , и мы можем сделать сет.
public static void reverse(List list) < rev(list); >private static void rev(List list) < Listtmp = new ArrayList(list); for (int i = 0; i < list.size(); i++) < list.set(i, tmp.get(list.size()-i-1)); >>
Теперь у нас все скомпилируется. Здесь произошел захват символа подстановки (wildcard capture). При вызове метода reverse(List list) в качестве аргумента передается список каких-то объектов (например, строк или целых чисел). Если мы можем захватить тип этих объектов и присвоить его переменной типа X , то можем заключить, что T является X .
Более подробно о Wildcard Capture можно прочитать здесь и здесь.
Вывод
Если необходимо читать из контейнера, то используйте wildcard с верхней границей " ? extends ". Если необходимо писать в контейнер, то используйте wildcard с нижней границей " ? super ". Не используйте wildcard, если нужно производить и запись, и чтение.
Не используйте Raw типы! Если аргумент типа не определен, то используйте wildcard .
Переменные типа
Когда мы записываем при объявлении класса или метода идентификатор в угловых скобках, например или , то создаем переменную типа. Переменная типа — это неквалифицированный идентификатор, который можно использовать в качестве типа в теле класса или метода. Переменная типа может быть ограничена сверху.
public static > T max(Collection coll) < T candidate = coll.iterator().next(); for (T elt : coll) < if (candidate.compareTo(elt) < 0) candidate = elt; >return candidate; >
В этом примере выражение T extends Comparable определяет T (переменную типа), ограниченную сверху типом Comparable . В отличие от wildcard, переменные типа могут быть ограничены только сверху (только extends ). Нельзя записать super . Кроме того, в этом примере T зависит от самого себя, это называется recursive bound — рекурсивная граница.
Вот еще пример из класса Enum:
public abstract class Enum>implements Comparable, Serializable
Здесь класс Enum параметризован типом E, который является подтипом от Enum .
Multiple bounds (множественные ограничения)
Multiple Bounds – множественные ограничения. Записывается через символ " & ", то есть мы говорим, что тип, представленный переменной типа T , должен быть ограничен сверху классом Object и интерфейсом Comparable .
> T max(Collection coll)
Вывод
Переменная типа может быть ограничена только сверху одним или несколькими типами. В случае множественного ограничения левая граница (первое ограничение) используется в процессе затирания (Type Erasure).
Type Erasure
Type Erasure представляет собой отображение типов (возможно, включая параметризованные типы и переменные типа) на типы, которые никогда не являются параметризованными типами или переменными типами. Мы записываем затирание типа T как |T| .
- Затиранием параметризованного типа GT1. Tn> является |G|
- Затиранием вложенного типа T.C является |T|.C
- Затиранием типа массива T[] является |T|[]
- Затиранием переменной типа является затирание ее левой границы
- Затиранием любого иного типа является сам этот тип
- добавляет приведение типов для обеспечения type safety, если это необходимо
- генерирует Bridge методы для сохранения полиморфизма
| T (Тип) | |T| (Затирание типа) |
| List< Integer>, List< String>, List< List< String>> | List |
| List< Integer>[] | List[] |
| List | List |
| int | int |
| Integer | Integer |
| > | Comparable |
| > | Object |
| LinkedCollection.Node | LinkedCollection.Node |
Эта таблица показывает, во что превращаются разные типы в процессе затирания, Type Erasure.

На скриншоте ниже два примера программы:
Разница между ними в том, что слева происходит compile-time error, а справа все компилируется без ошибок. Почему?
В Java два разных метода не могут иметь одну и ту же сигнатуру. В процессе Type Erasure компилятор добавит bridge-метод public int compareTo(Object o) . Но в классе уже содержится метод с такой сигнатурой, что и вызовет ошибку во время компиляции.
Скомпилируем класс Name, удалив метод compareTo(Object o) , и посмотрим на получившийся байткод с помощью javap:
# javap Name.class Compiled from "Name.java" public class ru.sberbank.training.generics.Name implements java.lang.Comparable
Видим, что класс содержит метод int compareTo(java.lang.Object) , хотя мы его удалили из исходного кода. Это и есть bridge метод, который добавил компилятор.
Reifiable типы
- Примитивные типы (int, long, boolean)
- Непараметризованные (необобщенные) типы (String, Integer)
- Параметризованные типы, параметры которых представлены в виде unbounded wildcard (неограниченных символов подстановки) (List , Collection )
- Raw (несформированные) типы (List, ArrayList)
- Массивы, компоненты которых — Reifiable типы (int[], Number[], List[], List[)
Почему информация об одних типах доступна, а о других нет? Дело в том, что из-за процесса затирания типов компилятором информация о некоторых типах может быть потеряна. Если она потерялась, то такой тип будет уже не reifiable. То есть она во время выполнения недоступна. Если доступна – соответственно, reifiable.
Решение не делать все обобщенные типы доступными во время выполнения — это одно из наиболее важных и противоречивых проектных решений в системе типов Java. Так сделали, в первую очередь, для совместимости с существующим кодом. За миграционную совместимость пришлось платить — полная доступность системы обобщенных типов во время выполнения невозможна.
- Переменная типа (T)
- Параметризованный тип с указанным типом параметра (List ArrayList , List>)
- Параметризованный тип с указанной верхней или нижней границей (List, Comparable ). Но здесь стоит оговориться: List — не reifiable, а List — reifiable
И еще одна задачка. Почему в примере ниже нельзя создать параметризованный Exception?
class MyException extends Exception
Каждое catch выражение в try-catch проверяет тип полученного исключения во время выполнения программы (что равносильно instanceof), соответственно, тип должен быть Reifiable. Поэтому Throwable и его подтипы не могут быть параметризованы.
class MyException extends Exception/ Generic class may not extend ‘java.lang.Throwable’ T t; >
Unchecked Warnings
Компиляция нашего приложения может выдать так называемый Unchecked Warning — предупреждение о том, что компилятор не смог корректно определить уровень безопасности использования наших типов. Это не ошибка, а предупреждение, так что его можно пропустить. Но желательно все-так исправить, чтобы избежать проблем в будущем.
Heap Pollution
Как мы упомянули ранее, присваивание ссылки на Raw тип переменной параметризованного типа, приводит к предупреждению «Unchecked assignment». Если мы проигнорируем его, то возможна ситуация под названием " Heap Pollution " (загрязнение кучи). Вот пример:
static List t() < List l = new ArrayList(); l.add(1); List ls = l; // (1) ls.add(""); return ls; >
В строке (1) компилятор предупреждает об «Unchecked assignment».
Нужно привести и другой пример «загрязнения кучи» — когда у нас используются параметризованные объекты. Кусок кода ниже наглядно показывает, что недопустимо использовать параметризованные типы в качестве аргументов метода с использованием Varargs . В данном случае параметр метода m – это List… , т.е. фактически, массив элементов типа List . Учитывая правило отображения типов при затирании, тип stringLists превращается в массив raw списков ( List[] ), т.е. можно выполнить присваивание Object[] array = stringLists; и после записать в array объект, отличный от списка строк (1), что вызовет ClassCastException в строке (2).
static void m(List. stringLists) < Object[] array = stringLists; ListtmpList = Arrays.asList(42); array[0] = tmpList; // (1) String s = stringLists[0].get(0); // (2) >
Рассмотрим еще один пример:
ArrayList strings = new ArrayList<>(); ArrayList arrayList = new ArrayList(); arrayList = strings; // (1) Ok arrayList.add(1); // (2) unchecked call
Java разрешает выполнить присваивание в строке (1). Это необходимо для обеспечения обратной совместимости. Но если мы попытаемся выполнить метод add в строке (2), то получим предупреждение Unchecked call — компилятор предупреждает нас о возможной ошибке. В самом деле, мы же пытаемся в список строк добавить целое число.
Reflection
Хотя при компиляции параметризованные типы подвергаются процедуре стирания (type erasure), кое-какую информацию мы можем получить с помощью Reflection.
- Все reifiable доступны через механизм Reflection
- Информация о типе полей класса, параметров методов и возвращаемых ими значений доступна через Reflection.
java.lang.reflect.Method.getGenericReturnType()
С появлением Generics класс java.lang.Class стал параметризованным. Рассмотрим вот этот код:
List ints = new ArrayList(); Class k = ints.getClass(); assert k == ArrayList.class;
Переменная ints имеет тип List и она содержит ссылку на объект типа ArrayList < Integer>. Тогда ints.getClass() вернёт объект типа Class , так как List затирается в List . Объект типа Class можно присвоить переменной k типа Class , согласно ковариантности символов подстановки? extends . А ArrayList.class возвращает объект типа Class .
Вывод
Если информация о типе доступна во время выполнения программы, то такой тип называется Reifiable. К Reifiable типам относятся: примитивные типы, непараметризованные типы, параметризованные типы с неограниченным символом подстановки, Raw типы и массивы, элементы которых являются reifiable.
Игнорирование Unchecked Warnings может привести к «загрязнению кучи» и ошибкам во время выполнения программы.
Reflection не позволяет получить информацию о типе объекта, если он не Reifiable. Но Reflection позволяет получить информацию о типе возвращаемого методом значения, о типе аргументов метода и о типе полей класса.
Type Inference
Термин можно перевести как «Вывод типа». Это возможность компилятора определять (выводить) тип из контекста. Вот пример кода:
List list = new ArrayList();
С появлением даймонд-оператора в Java 7 мы можем не указывать тип у ArrayList :
List list = new ArrayList<>();
Компилятор выведет тип ArrayList из контекста – List . Этот процесс и называется type inference .
- Приведение (reduction)
- Объединение (incorporation)
- Разрешение (resolution)
Предположим у нас есть вот такой класс, который описывает связный список:
class List < static List nil() < . >; static List cons(Z head, List tail) < . >; E head() < . >>
Результат обобщенного метода List.nil() может быть выведен из правой части:
List ls = List.nil();
Механизм выбора типа компилятором показывает, что аргумент типа для вызова List.nil() действительно String — это работает в JDK 7, все хорошо.
Выглядит разумно, что компилятор также должен иметь возможность вывести тип, когда результат такого вызова обобщенного метода передается другому методу в качестве аргумента, например:
List.cons(42, List.nil()); //error: expected List, found List
В JDK 7 мы получили бы compile-time error. А в JDK 8 скомпилируется. Это и есть первая часть JEP-101, его первая цель — вывод типа в позиции аргумента. Единственная возможность осуществить это в версиях до JDK 8 — использовать явный аргумент типа при вызове обобщенного метода:
List.cons(42, List.nil());
Вторая часть JEP-101 говорит о том, что неплохо бы выводить тип в цепочке вызовов обобщенных методов, например:
String s = List.nil().head(); //error: expected String, found Object
Но данная задача не решена до сих пор, и вряд ли в ближайшее время появится такая функция. Возможно, в будущих версиях JDK необходимость в этом исчезнет, но пока нужно указывать аргументы вручную:
String s = List.nil().head();
После выхода JEP 101 на StackOverflow появилось множество вопросов по теме. Программисты спрашивают, почему код, который выполнялся на 7-й версии, на 8-й выполняется иначе – или вообще не компилируется? Вот пример такого кода:
class Test < static void m(Object o) < System.out.println("one"); >static void m(String[] o) < System.out.println("two"); >static T g() < return null; >public static void main(String[] args) < m(g()); >>
Посмотрим на байт-код после компиляции на JDK1.8:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: invokestatic #6 // Method g:()Ljava/lang/Object; 3: checkcast #7 // class "[Ljava/lang/String;" 6: invokestatic #8 // Method m:([Ljava/lang/String;)V 9: return LineNumberTable: line 15: 0 line 16: 9
Инструкция под номером 0 выполняет вызов метода g:()Ljava/lang/Object; Метод возвращает java.lang.Object . Далее, инструкция 3 производит приведение типа («кастинг») объекта, полученного на предыдущем шаге к типу массива java.lang.String , и инструкция 6 выполняет метод m:([Ljava/lang/String;) , что и напечатает в консоли «two».
А теперь байт-код после компиляции на JDK1.7 – то есть на Java 7:
public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: invokestatic #6 // Method g:()Ljava/lang/Object; 3: invokestatic #7 // Method m:(Ljava/lang/Object;)V 6: return LineNumberTable: line 15: 0 line 16: 6
Мы видим, что здесь нет инструкции checkcast , которую добавила Java 8, так что вызовется метод m:(Ljava/lang/Object;) , а в консоли напечатается «one». Checkcast – результат нового выведения типа, который был усовершенствован в Java 8.
Чтобы избежать таких проблем, Oracle выпустил руководство по переходу с JDK1.7 на JDK 1.8 в котором описаны проблемы, которые могут возникнуть при переходе на новую версию Java, и то, как эти проблемы можно решить.
Например если вы хотите, чтобы в коде выше после компиляции на Java 8 все работало так же, как и на Java 7, сделайте приведение типа вручную:
public static void main(String[] args)
Заключение
На этом мой рассказ о Java Generics подходит к концу. Вот другие источники, которые помогут вам в освоении темы:
- Naftalin, Maurice; Wadler, Philip. Java Generics and Collections. O'Reilly Media. ISBN-13: 978-0596527754
- https://docs.oracle.com/javase/specs/jls/se8/html/index.html
- Язык программирования Java SE 8. Addison-Wesley. ISBN: 978-5-8459-1875-8
- Bloch, Joshua. Effective Java. Third Edition. Addison-Wesley. ISBN-13: 978-0-13-468599-1
Теория дженериков в Java или как на практике ставить скобки

Дженерики (обобщения) — это особые средства языка Java для реализации обобщённого программирования: особого подхода к описанию данных и алгоритмов, позволяющего работать с различными типами данных без изменения их описания. На сайте Oracle дженерикам посвящён отдельный tutorial: "Lesson: Generics".
Во-первых, чтобы понять дженерики, нужно разобраться, зачем они вообще нужны и что они дают. В tutorial в разделе "Why Use Generics?" сказано, что одно из назначений — более сильная проверка типов во время компиляции и устранение необходимости явного приведения.

Приготовим для опытов любимый tutorialspoint online java compiler. Представим себе такой вот код:
import java.util.*; public class HelloWorld < public static void main(String []args)< List list = new ArrayList(); list.add("Hello"); String text = list.get(0) + ", world!"; System.out.print(text); >>
Этот код выполнится хорошо. Но что если к нам пришли и сказали, что фраза "Hello, world!" избита и можно вернуть только Hello? Удалим из кода конкатенацию со строкой ", world!" . Казалось бы, что может быть безобиднее? Но на деле мы получим ошибку ПРИ КОМПИЛЯЦИИ: error: incompatible types: Object cannot be converted to String Всё дело в том, что в нашем случае List хранит список объектов типа Object. Так как String — наследник для Object (ибо все классы неявно наследуются в Java от Object), то требует явного приведения, чего мы не сделали. А при конкатенации для объекта будет вызван статический метод String.valueOf(obj), который в итоге вызовет метод toString для Object. То есть List у нас содержит Object. Выходит, там где нам нужен конкретный тип, а не Object, нам придётся самим делать приведение типов:
import java.util.*; public class HelloWorld < public static void main(String []args)< List list = new ArrayList(); list.add("Hello!"); list.add(123); for (Object str : list) < System.out.println((String)str); >> >
Однако, в данном случае, т.к. List принимает список объектов, он хранит не только String, но и Integer. Но самое плохое, в этом случае компилятор не увидит ничего плохого. И тут мы получим ошибку уже ВО ВРЕМЯ ВЫПОЛНЕНИЯ (ещё говорят, что ошибка получена "в Runtime"). Ошибка будет: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String Согласитесь, не самое приятное. И всё это потому, что компилятор — не искусcтвенный интеллект и он не может угадать всё, что подразумевает программист. Чтобы рассказать компилятору подробнее о своих намерениях, какие типы мы собираемся использовать, в Java SE 5 ввели дженерики. Исправим наш вариант, подсказав компилятору, что же мы хотим:
import java.util.*; public class HelloWorld < public static void main(String []args)< Listlist = new ArrayList<>(); list.add("Hello!"); list.add(123); for (Object str : list) < System.out.println(str); >> >
Как мы видим, нам больше не нужно приведение к String. Кроме того, у нас появились угловые скобки (angle brackets), которые обрамляют дженерики. Теперь компилятор не даст скомпилировать класс, пока мы не удалим добавление 123 в список, т.к. это Integer. Он нам так и скажет. Многие называют дженерики "синтаксическим сахаром". И они правы, так как дженерики действительно при компиляции станут теми самыми кастами. Посмотрим на байткод скомпилированных классов: с кастом вручную и с использованием дженериков:

После компиляции какая-либо информация о дженериках стирается. Это называется "Стирание типов" или "Type Erasure". Стирание типов и дженерики сделаны так, чтобы обеспечить обратную совместимость со старыми версиями JDK, но при этом дать возможность помогать компилятору с определением типа в новых версиях Java.

Raw Types или сырые типы
Говоря о дженериках мы всегда имеем две категории: типизированные типы (Generic Types) и "сырые" типы (Raw Types). Сырые типы — это типы без указания "уточненения" в фигурных скобках (angle brackets):

Типизированные типы — наоборот, с указанием "уточнения":

Как мы видим, мы использовали необычную конструкцию, отмеченную стрелкой на скриншоте. Это особый синтаксис, который добавили в Java SE 7, и называется он "the diamond", что в переводе означает алмаз. Почему? Можно провести аналогию формы алмаза и формы фигурных скобок: <> Также Diamond синтаксис связан с понятием "Type Inference", или же выведение типов. Ведь компилятор, видя справа <> смотрит на левую часть, где расположено объявление типа переменной, в которую присваивается значение. И по этой части понимает, каким типом типизируется значение справа. На самом деле, если в левой части указан дженерик, а справа не указан, компилятор сможет вывести тип:
import java.util.*; public class HelloWorld < public static void main(String []args) < Listlist = new ArrayList(); list.add("Hello World"); String data = list.get(0); System.out.println(data); > >
Однако это будет смешиванием нового стиля с дженериками и старого стиля без них. И это крайне нежелательно. При компиляции кода выше мы получим сообщение: Note: HelloWorld.java uses unchecked or unsafe operations . На самом деле кажется непонятным, зачем вообще нужен тут diamond добавлять. Но вот пример:
import java.util.*; public class HelloWorld < public static void main(String []args) < Listlist = Arrays.asList("Hello", "World"); List data = new ArrayList(list); Integer intNumber = data.get(0); System.out.println(data); > >
Как мы помним, у ArrayList есть и второй конструктор, который принимает на вход коллекцию. И вот тут-то и кроется коварство. Без diamond синтаксиса компилятор не понимает, что его обманывают, а вот с diamond — понимает. Поэтому, правило #1: всегда использовать diamond синтаксис, если мы используем типизированные типы. В противном случае мы рискуем пропустить, где у нас используется raw type. Чтобы избежать предупреждений в логе о том, что "uses unchecked or unsafe operations" можно над используемым методом или классом указать особую аннотацию: @SuppressWarnings("unchecked") Suppress переводится как подавлять, то есть дословно — подавить предупреждения. Но подумайте, почему вы решили её указать? Вспомните о правиле номер один и, возможно, вам нужно добавить типизацию.

Типизированные методы (Generic Methods)
- включает список типизированных параметров внутри угловых скобок;
- список типизированных параметров идёт до возвращаемого метода.
import java.util.*; public class HelloWorld < public static class Util < public static T getValue(Object obj, Class clazz) < return (T) obj; >public static T getValue(Object obj) < return (T) obj; >> public static void main(String []args) < List list = Arrays.asList("Author", "Book"); for (Object element : list) < String data = Util.getValue(element, String.class); System.out.println(data); System.out.println(Util.getValue(element)); > > >
Если посмотреть на класс Util, видим в нём два типизированных метода. Благодаря возможности выведения типов мы можем предоставить определение типа непосредственно компилятору, а можем сами это указать. Оба варианта представлены в примере. Кстати, синтаксис весьма логичен, если подумать. При типизировании метода мы указываем дженерик ДО метода, потому что если мы будем использовать дженерик после метода, Java не сможет понять, какой тип использовать. Поэтому сначала объявляем, что будем использовать дженерик T, а потом уже говорим, что этот дженерик мы собираемся возвращать. Естественно, Util.
import java.util.*; public class HelloWorld < public static class Util < public static T getValue(Object obj) < return (T) obj; >> public static void main(String []args) < List list = Arrays.asList(2, 3); for (Object element : list) < System.out.println(Util.getValue(element) + 1); > > >
Он будет прекрасно работать. Но только до тех пор, пока компилятор будет понимать, что у вызываемого метода тип Integer. Заменим вывод на консоль на следующую строку: System.out.println(Util.getValue(element) + 1); И мы получим ошибку: bad operand types for binary operator '+', first type: Object , second type: int То есть произошло стирание типов. Компилятор видит, что тип никто не указал, тип указывается как Object и выполнение кода падает с ошибкой.

Типизированные классы (Generic Types)
Типизировать можно не только методы, но и сами классы. У Oracle в их гайде этому посвящён раздел "Generic Types". Рассмотрим пример:
public static class SomeType < public void test(Collection collection) < for (E element : collection) < System.out.println(element); >> public void test(List collection) < for (Integer element : collection) < System.out.println(element); >> >
Тут всё просто. Если мы используем класс, дженерик указывается после имени класса. Давайте теперь в методе main создадим экземпляр этого класса:
public static void main(String []args) < SomeTypest = new SomeType<>(); List list = Arrays.asList("test"); st.test(list); >
Он отработает хорошо. Компилятор видит, что есть List из чисел и Collection типа String. Но что если мы сотрём дженерики и сделаем так:
SomeType st = new SomeType(); List list = Arrays.asList("test"); st.test(list);
Мы получим ошибку: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer Опять стирание типов. Поскольку у класса больше нет дженерика, компилятор решает: раз мы передали List, метод с List
Ограничения
К типам, указываемым в дженериках мы можем применить ограничение. Например, мы хотим, чтобы контейнер принимал на вход только Number. Данная возможность описана в Oracle Tutorial в разделе Bounded Type Parameters. Посмотрим на пример:
import java.util.*; public class HelloWorld < public static class NumberContainer < private T number; public NumberContainer(T number) < this.number = number; >public void print() < System.out.println(number); >> public static void main(String []args) < NumberContainer number1 = new NumberContainer(2L); NumberContainer number2 = new NumberContainer(1); NumberContainer number3 = new NumberContainer("f"); >>
- Upper Bounded Wildcards -
- Unbounded Wildcards -
- Lower Bounded Wildcards -

Данный принцип ещё называют принципом PECS (Producer Extends Consumer Super). Подробнее можно прочитать на хабре в статье "Использование generic wildcards для повышения удобства Java API", а также в отличном обсуждении на stackoverflow: "Использование wildcard в Generics Java". Вот небольшой пример из исходников Java — метод Collections.copy:

Ну и небольшой примерчик того, как НЕ будет работать:
public static class TestClass < public static void print(List extends String>list) < list.add("Hello World!"); System.out.println(list.get(0)); >> public static void main(String []args) < Listlist = new ArrayList<>(); TestClass.print(list); >
Но если заменить extends на super, всё станет хорошо. Так как мы наполняем список list значением перед выводом, он для нас является потребителем, то есть consumer'ом. Следовательно, используем super.
Наследование
Есть ещё одна необычная особенность дженериков - это их наследование. Наследование дженериков описано в tutorial от Oracle в разделе "Generics, Inheritance, and Subtypes". Главное это запомнить и осознать следующее. Мы не можем сделать так:
List list1 = new ArrayList();
Потому что наследование работает с дженериками по-другому:

И вот ещё хороший пример, который упадёт с ошибкой:
List list1 = new ArrayList<>(); List list2 = list1;
Тут тоже всё просто. List
Final
- Юрий Ткач: Сырые типы - Generics #1 - Advanced Java
- Наследование и расширители обобщений - Generics #2 - Advanced Java
- Рекурсивное расширение типа - Generics #3 - Advanced Java
- Александр Маторин — Неочевидные Дженерики
- Введение в Java. Generics. Wildcards | Технострим
- O'Reilly : Java Generics and Collections