Generics
Generics — набор свойств языка позволяющих определять и использовать обобщенные типы и методы.
Обобщенные типы или методы отличаются от обычных тем, что имеют типизированные параметры (T – параметр в котором могут быть разные объекты).
ArrayList<Integer> list = new ArrayList<Integer>();
- Строгая типизация.
- Единая реализация.
- Отсутствие информации о типе.
В <> могут быть только ссылочные типы: можно: String, Integer, int[] , нельзя: int, double .
Пусть у нас есть тип Foo, который является подтипом Bar, и еще G — наследник Коллекции.
То G<Foo> не является наследником G<Bar>.
public class Example <
public void print(Set<String> strSet)
public void print(Set<Integer> intSet)
Примером использования обобщенных типов может служить Java Collection Framework.
Так, класс LinkedList<E> — типичный обобщенный тип. Он содержит параметр E, который представляет тип элементов, которые будут храниться в коллекции.
Создание объектов обобщенных типов происходит посредством замены параметризированных типов реальными типами данных.
Вместо того, чтобы просто использовать LinkedList, ничего не говоря о типе элемента в списке, предлагается использовать точное указание типа LinkedList<String>, LinkedList<Integer> и т.п.
Для чего нужны дженерики?
Generics (обобщенные типы и методы) позволяют нам уйти от жесткого определения используемых типов.
С их помощью можно объявлять классы, интерфейсы и методы, где тип данных указан в виде параметра.
Возможность создавать универсальные алгоритмы и структуры данных.
Позволяет осуществлять проверку на правильность написания кода во время компиляции, а не в Runtime.
Компилятор стирает все дженерики. В Runtime дженериков практически нет. Компилятор использует кастование.
Хоть бы слово про стирание типов.
Сделали использование Java Collection Framework проще, удобнее и безопаснее.
Стирание типов
Это когда при компиляции из обобщенных классов с параметрами генерируется код обычных сырых классов с расстановкой, где нужно, приведения типов, генерацией дополнительных методов и т.д.
Дженерики существуют только на этапе компиляции и только для компилятора.
Стираются вверх (т.е. если не указано extends, то стирается в Object)
Что такое сырые типы (raw type)?
Raw type — это имя интерфейса без указания параметризованного типа
Сырые типы — это типы без указания «уточненения» в фигурных скобках.
Нужны чтобы поддерживать старый код (обратная совместимость).
Рекомендуется использовать хоть какие-то параметризованные типы.
Также Diamond синтаксис связан с понятием «Type Inference», или же выведение типов. Ведь компилятор, видя справа <> смотрит на левую часть, где расположено объявление типа переменной, в которую присваивается значение. И по этой части понимает, каким типом типизируется значение справа.
На самом деле, если в левой части указан дженерик, а справа не указан <> , компилятор сможет вывести тип. Однако это будет смешиванием нового стиля с дженериками и старого стиля без них — вы теряете безопасность (типобезопасность) типов (может добавляться какой угодно тип в список, а не то что надо. Когда будет доставаться – то может быть сюрприз)))
ArrayList <String> strings = new ArrayList<>(); // parameterized type
ArrayList arrayList = new ArrayList(); // raw type
arrayList = strings; // Ok
strings = arrayList; // Unchecked assignment (назначение)
arrayList.add(1); //unchecked call
Что такое вайлдкарды (Маски)
Языковая конструкция внутри diamond-оператора, позволяющая сделать код более универсальным.
Решает проблему наследования типов в дженериках.
Может быть 3-х типов: инвариантность, upper и lower.
Принцип PECS
The Get and Put Principle или PECS (Producer Extends Consumer Super) Get and Put Principe
Из одного типа переменных можно только читать, в другой — только вписывать (исключением является возможность записать null для extends и прочитать Object для super).
Запись вида Collection<?> равносильна Collection<? extends Object> , а значит — коллекция может содержать объекты любого класса, так как все классы в Java наследуются от Object – поэтому подстановка называется неограниченной.
Если мы объявили wildcard с extends, то это producer. Он только «продюсирует», предоставляет элемент из контейнера, а сам ничего не принимает.
Если же мы объявили wildcard с super — то это consumer. Он только принимает, а предоставить ничего не может.
Если метод имеет аргументы с параметризованным типом (например, Collection или Predicate), то в случае, если аргумент — производитель (producer), нужно использовать ? extends T, а если аргумент — потребитель (consumer), нужно использовать ? super T.
Eсли метод читает данные из аргумента, то этот аргумент — производитель, Метод передаёт данные в аргумент, то аргумент является потребителем. Важно заметить, что определяя производителя или потребителя, мы рассматриваем только данные типа T.
Что такое дженерики в Java?
Рассмотрим пример, в котором вы должны составить список живых существ в местности. Неважно, человек это, животное или растение. Все, что имеет значение, является живым существом. В этом случае вы бы сгруппировали их как «живые существа», а не классифицировали их.
Точно так же, когда вам нужно хранить некоторые данные, для вас важен контент, а не тип данных, и именно здесь вы используете дженерики. Обобщения в Java – это языковая функция, которая позволяет использовать универсальные типы и методы.
Что такое Generics в Java?
Дженерики в Java – это термин, обозначающий набор языковых возможностей, связанных с определением и использованием общих типов и методов. Общие методы Java отличаются от обычных типов данных и методов. До Generics мы использовали коллекцию для хранения любых типов объектов, т.е. неуниверсальных. Теперь Generics заставляет программиста Java хранить объекты определенного типа.
Если вы посмотрите на классы платформы Java-коллекции, то увидите, что большинство классов принимают параметр / аргумент типа Object. По сути, в этой форме они могут принимать любой тип Java в качестве аргумента и возвращать один и тот же объект или аргумент. Они в основном неоднородны, т.е. не похожего типа.
Иногда в приложении Java тип данных ввода не является фиксированным. Входными данными могут быть целое число, число с плавающей запятой или строка. Чтобы назначить ввод переменной правильного типа данных, необходимо было провести предварительные проверки.
В традиционном подходе после получения ввода проверяется тип данных ввода, а затем назначается переменная правого типа данных. При использовании этой логики длина кода и время выполнения были увеличены. Чтобы избежать этого, были введены дженерики.
Когда вы используете Generics, параметры в коде автоматически проверяются во время компиляции, и он устанавливает тип данных по умолчанию. Так что это то место, где вам нужна концепция обобщений в Java.
Существует 4 различных способа применения:
- Типовой класс
- Интерфейс
- Метод
- Конструктор
1. Типовой класс
Класс называется дженериком, если он объявляет одну или несколько переменных типа. Эти типы переменных известны как параметры типа класса Java. Давайте разберемся с этим на примере. В приведенном ниже примере я создам класс с одним свойством x, а типом свойства является объект.
Здесь, как только вы инициализируете класс с определенным типом, класс должен использоваться только с этим конкретным типом. Например, если вы хотите, чтобы один экземпляр класса содержал значение x типа ‘String’, программист должен установить и получить единственный тип String.
Поскольку я объявил тип свойства для объекта, нет никакого способа применить это ограничение. Программист может установить любой объект и может ожидать любой тип возвращаемого значения от метода get, поскольку все типы Java являются подтипами класса Object.
Чтобы применить этот тип ограничения, мы можем использовать обобщенные значения, как показано ниже:
Теперь вы можете быть уверены, что класс не будет неправильно использоваться с неправильными типами. Простой пример «Genericclass» выглядит так, как показано ниже:
Эта аналогия верна и для интерфейса.
2. Интерфейс
Интерфейс в Java относится к абстрактным типам данных. Они позволяют манипулировать коллекциями Java независимо от деталей их представления.
Кроме того, они образуют иерархию в объектно-ориентированных языках программирования.
3. Методы
Универсальные методы очень похожи на универсальные классы. Они отличаются друг от друга только одним аспектом, заключающимся в том, что информация о области действия или типе находится только внутри метода. Универсальные методы вводят свои параметры типа.
Если вы передадите список String для поиска в этом методе, он будет работать нормально. Но если вы попытаетесь найти число в списке строк, это даст ошибку времени компиляции.
4. Конструктор
Конструктор Java – это блок кода, который инициализирует вновь созданный объект. Конструктор напоминает метод экземпляра в Java, но это не метод, поскольку он не имеет возвращаемого типа. Конструктор имеет то же имя, что и класс, и выглядит так в коде Java.
В приведенном выше примере конструктор класса Dimension содержит информацию о типе. Таким образом, вы можете иметь экземпляр измерения со всеми атрибутами только одного типа.
Преимущества дженериков в Java
1. Повторное использование кода.
Вы можете составить стратегию, класс или интерфейс один раз и использовать их для любого типа или любым другим способом.
2. Кастинг отдельных типов не требуется.
По сути, вы восстанавливаете информацию из ArrayList каждый раз, когда вам нужно ее типизировать.
Типирование при каждой задаче восстановления является серьезной задачей. Чтобы искоренить этот подход, были введены дженерики.
3. Реализация неуниверсального алгоритма.
Он может рассчитывать алгоритмы, которые работают с различными типами элементов, которые также являются безопасными типами.
Пришел, увидел, обобщил: погружаемся в Java Generics
Java Generics — это одно из самых значительных изменений за всю историю языка Java. «Дженерики», доступные с Java 5, сделали использование Java Collection Framework проще, удобнее и безопаснее. Ошибки, связанные с некорректным использованием типов, теперь обнаруживаются на этапе компиляции. Да и сам язык Java стал еще безопаснее. Несмотря на кажущуюся простоту обобщенных типов, многие разработчики сталкиваются с трудностями при их использовании. В этом посте я расскажу об особенностях работы с Java Generics, чтобы этих трудностей у вас было поменьше. Пригодится, если вы не гуру в дженериках, и поможет избежать много трудностей при погружении в тему.
Работа с коллекциями
Предположим, банку нужно подсчитать сумму сбережений на счетах клиентов. До появления «дженериков» метод вычисления суммы выглядел так:
Мы итерировались, пробегались по списку аккаунтов и проверяли, действительно ли элемент из этого списка является экземпляром класса Account — то есть счетом пользователя. Выполняли приведение типа нашего объекта класса Account и метод getAmount , который возвращал сумму на этом счете. Дальше все это суммировали и возвращали итоговую сумму. Требовалось выполнить два действия:
Если не сделать проверку ( instanceof ) на принадлежность к классу Account , то на втором этапе возможен ClassCastException – то есть аварийное завершение программы. Поэтому такая проверка была обязательной.
С появлением Generics необходимость в проверке и приведении типа отпала:
Теперь метод принимает в качестве аргументов только список объектов класса Account . Это ограничение указано в самом методе, в его сигнатуре, программист просто не может передать никакой другой список — только список клиентских счетов.
Нам не нужно выполнять проверку типа элементов из этого списка: она подразумевается описанием типа у параметра метода (можно прочитать как список объектов класса Account ). И компилятор выдаст ошибку, если что-то пойдет не так — то есть если кто-то попробует передать в этот метод список объектов, отличных от класса Account .
Во второй строчке проверки необходимость тоже отпадала. Если потребуется, приведение типов ( casting ) будет сделано на этапе компиляции.
Принцип подстановки
Принцип подстановки Барбары Лисков – специфичное определение подтипа в объектно-ориентированном программировании. Идея Лисков о «подтипе» дает определение понятия замещения: если S является подтипом T , тогда объекты типа T в программе могут быть замещены объектами типа S без каких-либо изменений желательных свойств этой программы.
| Тип | Подтип |
| Number | Integer |
| List<E> | ArrayList<E> |
| Collection<E> | List<E> |
| Iterable<E> | Collection<E> |
Примеры отношения тип/подтип
Вот пример использования принципа подстановки в Java:
Integer является подтипом Number , следовательно, переменной n типа Number можно присвоить значение, которое возвращает метод Integer.valueOf(42) .
Ковариантность, контравариантность и инвариантность
Сначала немного теории. Ковариантность — это сохранение иерархии наследования исходных типов в производных типах в том же порядке. Например, если Кошка — это подтип Животные, то Множество<Кошки> — это подтип Множество<Животные>. Следовательно, с учетом принципа подстановки можно выполнить такое присваивание:
Множество<Животные> = Множество<Кошки>
Контравариантность — это обращение иерархии исходных типов на противоположную в производных типах. Например, если Кошка — это подтип Животные , то Множество<Животные> — это подтип Множество<Кошки>. Следовательно, с учетом принципа подстановки можно выполнить такое присваивание:
Множество<Кошки> = Множество<Животные>
Инвариантность — отсутствие наследования между производными типами. Если Кошка — это подтип Животные, то Множество<Кошки> не является подтипом Множество<Животные> и Множество<Животные> не является подтипом Множество<Кошки>.
Массивы в Java ковариантны. Тип S[] является подтипом T[] , если S — подтип T . Пример присваивания:
Мы присвоили ссылку на массив строк переменной arr , тип которой – «массив объектов» . Если бы массивы не были ковариантными, нам бы это сделать не удалось. Java позволяет это сделать, программа скомпилируется и выполнится без ошибок.
Но если мы попытаемся изменить содержимое массива через переменную arr и запишем туда число 42, то получим ArrayStoreException на этапе выполнения программы, поскольку 42 является не строкой, а числом. В этом недостаток ковариантности массивов Java: мы не можем выполнить проверки на этапе компиляции, и что-то может сломаться уже в рантайме.
«Дженерики» инвариантны. Приведем пример:
Если взять список целых чисел, то он не будет являться ни подтипом типа Number , ни каким-либо другим подтипом. Он является только подтипом самого себя. То есть List <Integer> — это List<Integer> и ничего больше. Компилятор позаботится о том, чтобы переменная ints , объявленная как список объектов класса Integer, содержала только объекты класса Integer и ничего кроме них. На этапе компиляции производится проверка, и у нас в рантайме уже ничего не упадет.
Wildcards
Всегда ли Generics инварианты? Нет. Приведу примеры:
Это ковариантность. List<Integer> — подтип List<? extends Number>
Это контравариантность. List<Number> является подтипом List<? super Integer> .
Запись вида «? extends . » или «? super . » — называется wildcard или символом подстановки, с верхней границей ( extends ) или с нижней границей ( super ). List<? extends Number> может содержать объекты, класс которых является Number или наследуется от Number . List<? super Number> может содержать объекты, класс которых Number или у которых Number является наследником (супертип от Number ).
Запись вида T2 <= T1 означает, что набор типов описываемых T2 является подмножеством набора типов описываемых T1
т.е.
Number <=? extends Object
? extends Number <=? extends Object
и
? super Object <=? super Number
Пара задачек для проверки знаний:
1. Почему в примере ниже compile-time error? Какое значение можно добавить в список nums ?
2. Почему нельзя получить элемент из списка ниже?
Нельзя прочитать элемент из контейнера с wildcard ? super , кроме объекта класса Object
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. Он только принимает, а предоставить ничего не может.
Метод осуществляет копирование элементов из исходного списка src в список dest . src — объявлен с wildcard ? extends и является продюсером, а dest — объявлен с wildcard ? super и является потребителем. Учитывая ковариантность и контравариантность wildcard, можно скопировать элементы из списка ints в список nums :
Если же мы по ошибке перепутаем параметры метода copy и попытаемся выполнить копирование из списка nums в список ints , то компилятор не позволит нам это сделать:
<?> и Raw типы
Ниже приведен wildcard с неограниченным символом подстановки. Мы просто ставим <?> , без ключевых слов super или extends :
На самом деле такой «неограниченный» wildcard все-таки ограничен, сверху. Collection<?> — это тоже символ подстановки, как и » ? extends Object «. Запись вида Collection<?> равносильна Collection<? extends Object> , а значит — коллекция может содержать объекты любого класса, так как все классы в Java наследуются от Object – поэтому подстановка называется неограниченной.
Если мы опустим указание типа, например, как здесь:
то, говорят, что ArrayList — это Raw тип параметризованного ArrayList<T>. Используя Raw типы, мы возвращаемся в эру до дженериков и сознательно отказываемся от всех фич, присущих параметризованным типам.
Если мы попытаемся вызвать параметризованный метода у Raw типа, то компилятор выдаст нам предупреждение «Unchecked call». Если мы попытаемся выполнить присваивание ссылки на параметризованный тип Raw типу, то компилятор выдаст предупреждение «Unchecked assignment». Игнорирование этих предупреждений, как мы увидим позже, может привести к ошибкам во время выполнения нашего приложения.
Wildcard Capture
Попробуем теперь реализовать метод, выполняющий перестановку элементов списка в обратном порядке.
Ошибка компиляции возникла, потому что в методе reverse в качестве аргумента принимается список с неограниченным символом подстановки <?> .
<?> означает то же что и <? extends Object> . Следовательно, согласно принципу PECS, list – это producer . А producer только продюсирует элементы. А мы в цикле for вызываем метод set() , т.е. пытаемся записать в list . И поэтому упираемся в защиту Java, что не позволяет установить какое-то значение по индексу.
Что делать? Нам поможет паттерн Wildcard Capture . Здесь мы создаем обобщенный метод rev . Он объявлен с переменной типа T . Этот метод принимает список типов T , и мы можем сделать сет.
Теперь у нас все скомпилируется. Здесь произошел захват символа подстановки (wildcard capture). При вызове метода reverse(List<?> list) в качестве аргумента передается список каких-то объектов (например, строк или целых чисел). Если мы можем захватить тип этих объектов и присвоить его переменной типа X , то можем заключить, что T является X .
Более подробно о Wildcard Capture можно прочитать здесь и здесь.
Вывод
Если необходимо читать из контейнера, то используйте wildcard с верхней границей » ? extends «. Если необходимо писать в контейнер, то используйте wildcard с нижней границей » ? super «. Не используйте wildcard, если нужно производить и запись, и чтение.
Не используйте Raw типы! Если аргумент типа не определен, то используйте wildcard <?> .
Переменные типа
Когда мы записываем при объявлении класса или метода идентификатор в угловых скобках, например <T> или <E> , то создаем переменную типа. Переменная типа — это неквалифицированный идентификатор, который можно использовать в качестве типа в теле класса или метода. Переменная типа может быть ограничена сверху.
В этом примере выражение T extends Comparable<T> определяет T (переменную типа), ограниченную сверху типом Comparable<T> . В отличие от wildcard, переменные типа могут быть ограничены только сверху (только extends ). Нельзя записать super . Кроме того, в этом примере T зависит от самого себя, это называется recursive bound — рекурсивная граница.
Вот еще пример из класса Enum:
Здесь класс Enum параметризован типом E, который является подтипом от Enum<E> .
Multiple bounds (множественные ограничения)
Multiple Bounds – множественные ограничения. Записывается через символ » & «, то есть мы говорим, что тип, представленный переменной типа T , должен быть ограничен сверху классом Object и интерфейсом Comparable .
Запись Object & Comparable<? super T> образует тип пересечения Multiple Bounds . Первое ограничение — в данном случае Object — используется для erasure , процесса затирания типов. Его выполняет компилятор на этапе компиляции.
Вывод
Переменная типа может быть ограничена только сверху одним или несколькими типами. В случае множественного ограничения левая граница (первое ограничение) используется в процессе затирания (Type Erasure).
Type Erasure
Type Erasure представляет собой отображение типов (возможно, включая параметризованные типы и переменные типа) на типы, которые никогда не являются параметризованными типами или переменными типами. Мы записываем затирание типа T как |T| .
- Затиранием параметризованного типа G<T1. 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 |
| <T extends Comparable<T>> | Comparable |
| <T extends Object & Comparable<? super T>> | Object |
| LinkedCollection<E>.Node | LinkedCollection.Node |
Эта таблица показывает, во что превращаются разные типы в процессе затирания, Type Erasure.
На скриншоте ниже два примера программы: 
Разница между ними в том, что слева происходит compile-time error, а справа все компилируется без ошибок. Почему?
В Java два разных метода не могут иметь одну и ту же сигнатуру. В процессе Type Erasure компилятор добавит bridge-метод public int compareTo(Object o) . Но в классе уже содержится метод с такой сигнатурой, что и вызовет ошибку во время компиляции.
Скомпилируем класс Name, удалив метод compareTo(Object o) , и посмотрим на получившийся байткод с помощью javap:
Видим, что класс содержит метод 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<Number>ArrayList<String>, List<List<String>>)
- Параметризованный тип с указанной верхней или нижней границей (List<? extends Number>, Comparable<? super String>). Но здесь стоит оговориться: List<? extends Object> — не reifiable, а List<?> — reifiable
И еще одна задачка. Почему в примере ниже нельзя создать параметризованный Exception?
Каждое catch выражение в try-catch проверяет тип полученного исключения во время выполнения программы (что равносильно instanceof), соответственно, тип должен быть Reifiable. Поэтому Throwable и его подтипы не могут быть параметризованы.
Unchecked Warnings
Компиляция нашего приложения может выдать так называемый Unchecked Warning — предупреждение о том, что компилятор не смог корректно определить уровень безопасности использования наших типов. Это не ошибка, а предупреждение, так что его можно пропустить. Но желательно все-так исправить, чтобы избежать проблем в будущем.
Heap Pollution
Как мы упомянули ранее, присваивание ссылки на Raw тип переменной параметризованного типа, приводит к предупреждению «Unchecked assignment». Если мы проигнорируем его, то возможна ситуация под названием » Heap Pollution » (загрязнение кучи). Вот пример:
В строке (1) компилятор предупреждает об «Unchecked assignment».
Нужно привести и другой пример «загрязнения кучи» — когда у нас используются параметризованные объекты. Кусок кода ниже наглядно показывает, что недопустимо использовать параметризованные типы в качестве аргументов метода с использованием Varargs . В данном случае параметр метода m – это List<String>… , т.е. фактически, массив элементов типа List<String> . Учитывая правило отображения типов при затирании, тип stringLists превращается в массив raw списков ( List[] ), т.е. можно выполнить присваивание Object[] array = stringLists; и после записать в array объект, отличный от списка строк (1), что вызовет ClassCastException в строке (2).
Рассмотрим еще один пример:
Java разрешает выполнить присваивание в строке (1). Это необходимо для обеспечения обратной совместимости. Но если мы попытаемся выполнить метод add в строке (2), то получим предупреждение Unchecked call — компилятор предупреждает нас о возможной ошибке. В самом деле, мы же пытаемся в список строк добавить целое число.
Reflection
Хотя при компиляции параметризованные типы подвергаются процедуре стирания (type erasure), кое-какую информацию мы можем получить с помощью Reflection.
- Все reifiable доступны через механизм Reflection
- Информация о типе полей класса, параметров методов и возвращаемых ими значений доступна через Reflection.
С появлением Generics класс java.lang.Class стал параметризованным. Рассмотрим вот этот код:
Переменная ints имеет тип List<Integer> и она содержит ссылку на объект типа ArrayList< Integer> . Тогда ints.getClass() вернёт объект типа Class<ArrayLis> , так как List<Integer> затирается в List . Объект типа Class<ArrayList> можно присвоить переменной k типа Class<? extends List> , согласно ковариантности символов подстановки? extends . А ArrayList.class возвращает объект типа Class<ArrayList> .
Вывод
Если информация о типе доступна во время выполнения программы, то такой тип называется Reifiable. К Reifiable типам относятся: примитивные типы, непараметризованные типы, параметризованные типы с неограниченным символом подстановки, Raw типы и массивы, элементы которых являются reifiable.
Игнорирование Unchecked Warnings может привести к «загрязнению кучи» и ошибкам во время выполнения программы.
Reflection не позволяет получить информацию о типе объекта, если он не Reifiable. Но Reflection позволяет получить информацию о типе возвращаемого методом значения, о типе аргументов метода и о типе полей класса.
Type Inference
Термин можно перевести как «Вывод типа». Это возможность компилятора определять (выводить) тип из контекста. Вот пример кода:
С появлением даймонд-оператора в Java 7 мы можем не указывать тип у ArrayList :
Компилятор выведет тип ArrayList из контекста – List<Integer> . Этот процесс и называется type inference .
- Приведение (reduction)
- Объединение (incorporation)
- Разрешение (resolution)
Предположим у нас есть вот такой класс, который описывает связный список:
Результат обобщенного метода List.nil() может быть выведен из правой части:
Механизм выбора типа компилятором показывает, что аргумент типа для вызова List.nil() действительно String — это работает в JDK 7, все хорошо.
Выглядит разумно, что компилятор также должен иметь возможность вывести тип, когда результат такого вызова обобщенного метода передается другому методу в качестве аргумента, например:
В JDK 7 мы получили бы compile-time error. А в JDK 8 скомпилируется. Это и есть первая часть JEP-101, его первая цель — вывод типа в позиции аргумента. Единственная возможность осуществить это в версиях до JDK 8 — использовать явный аргумент типа при вызове обобщенного метода:
Вторая часть JEP-101 говорит о том, что неплохо бы выводить тип в цепочке вызовов обобщенных методов, например:
Но данная задача не решена до сих пор, и вряд ли в ближайшее время появится такая функция. Возможно, в будущих версиях JDK необходимость в этом исчезнет, но пока нужно указывать аргументы вручную:
После выхода JEP 101 на StackOverflow появилось множество вопросов по теме. Программисты спрашивают, почему код, который выполнялся на 7-й версии, на 8-й выполняется иначе – или вообще не компилируется? Вот пример такого кода:
Посмотрим на байт-код после компиляции на JDK1.8:
Инструкция под номером 0 выполняет вызов метода g:()Ljava/lang/Object; Метод возвращает java.lang.Object . Далее, инструкция 3 производит приведение типа («кастинг») объекта, полученного на предыдущем шаге к типу массива java.lang.String , и инструкция 6 выполняет метод m:([Ljava/lang/String;) , что и напечатает в консоли «two».
А теперь байт-код после компиляции на JDK1.7 – то есть на Java 7:
Мы видим, что здесь нет инструкции checkcast , которую добавила Java 8, так что вызовется метод m:(Ljava/lang/Object;) , а в консоли напечатается «one». Checkcast – результат нового выведения типа, который был усовершенствован в Java 8.
Чтобы избежать таких проблем, Oracle выпустил руководство по переходу с JDK1.7 на JDK 1.8 в котором описаны проблемы, которые могут возникнуть при переходе на новую версию Java, и то, как эти проблемы можно решить.
Например если вы хотите, чтобы в коде выше после компиляции на Java 8 все работало так же, как и на Java 7, сделайте приведение типа вручную:
Заключение
На этом мой рассказ о Java Generics подходит к концу. Вот другие источники, которые помогут вам в освоении темы:
Что такое дженерики какую проблему они решают

Обобщения или generics (обобщенные типы и методы) позволяют нам уйти от жесткого определения используемых типов. Рассмотрим проблему, в которой они нам могут понадобиться.
Допустим, мы определяем класс для представления банковского счета. К примеру, он мог бы выглядеть следующим образом:
Класс Account имеет два поля: id — уникальный идентификатор счета и sum — сумма на счете.
В данном случае идентификатор задан как целочисленное значение, например, 1, 2, 3, 4 и так далее. Однако также нередко для идентификатора используются и строковые значения. И числовые, и строковые значения имеют свои плюсы и минусы. И на момент написания класса мы можем точно не знать, что лучше выбрать для хранения идентификатора — строки или числа. Либо, возможно, этот класс будет использоваться другими разработчиками, которые могут иметь свое мнение по данной проблеме. Например, в качестве типа id они захотят использовать какой-то свой класс.
И на первый взгляд мы можем решить данную проблему следующим образом: задать id как поле типа Object, который является универсальным и базовым суперклассом для всех остальных типов:
В данном случае все замечательно работает. Однако тогда мы сталкиваемся с проблемой безопасности типов . Например, в следующем случае мы получим ошибку:
Проблема может показаться искуственной, так как в данном случае мы видим, что в конструктор передается строка, поэтому мы вряд ли будем пытаться преобразовывать ее к типу int. Однако в процессе разработки мы можем не знать, какой именно тип представляет значение в id, и при попытке получить число в данном случае мы столкнемся с исключением java.lang.ClassCastException.
Писать для каждого отдельного типа свою версию класса Account тоже не является хорошим решением, так как в этом случае мы вынуждены повторяться.
Эти проблемы были призваны устранить обобщения или generics. Обобщения позволяют не указывать конкретный тип, который будет использоваться. Поэтому определим класс Account как обобщенный:
С помощью буквы T в определении класса class Account<T> мы указываем, что данный тип T будет использоваться этим классом. Параметр T в угловых скобках называется универсальным параметром , так как вместо него можно подставить любой тип. При этом пока мы не знаем, какой именно это будет тип: String, int или какой-то другой. Причем буква T выбрана условно, это может и любая другая буква или набор символов.
После объявления класса мы можем применить универсальный параметр T : так далее в классе объявляется переменная этого типа, которой затем присваивается значение в конструкторе.
Метод getId() возвращает значение переменной id, но так как данная переменная представляет тип T, то данный метод также возвращает объект типа T: public T getId() .
Используем данный класс:
При определении переменной даннного класса и создании объекта после имени класса в угловых скобках нужно указать, какой именно тип будет использоваться вместо универсального параметра. При этом надо учитывать, что они работают только с объектами, но не работают с примитивными типами. То есть мы можем написать Account<Integer> , но не можем использовать тип int или double, например, Account<int> . Вместо примитивных типов надо использовать классы-обертки: Integer вместо int, Double вместо double и т.д.
Например, первый объект будет использовать тип String, то есть вместо T будет подставляться String:
В этом случае в качестве первого параметра в конструктор передается строка.
А второй объект использует тип int (Integer):
Обобщенные интерфейсы
Интерфейсы, как и классы, также могут быть обобщенными. Создадим обобщенный интерфейс Accountable и используем его в программе:
При реализации подобного интерфейса есть две стратегии. В данном случае реализована первая стратегия, когда при реализации для универсального параметра интерфейса задается конкретный тип, как например, в данном случае это тип String. Тогда класс, реализующий интерфейс, жестко привязан к этому типу.
Вторая стратегия представляет определение обобщенного класса, который также использует тот же универсальный параметр:
Обобщенные методы
Кроме обобщенных типов можно также создавать обобщенные методы, которые точно также будут использовать универсальные параметры. Например:
Особенностью обобщенного метода является использование универсального параметра в объявлении метода после всех модификаторов и перед типом возвращаемого значения.
Затем внутри метода все значения типа T будут представлять данный универсальный параметр.
При вызове подобного метода перед его именем в угловых скобках указывается, какой тип будет передаваться на место универсального параметра:
Использование нескольких универсальных параметров
Мы можем также задать сразу несколько универсальных параметров:
В данном случае тип String будет передаваться на место параметра T, а тип Double — на место параметра S.
Обобщенные конструкторы
Конструкторы как и методы также могут быть обобщенными. В этом случае перед конструктором также указываются в угловых скобках универсальные параметры:
В данном случае конструктор принимает параметр id, который представляет тип T. В конструкторе его значение превращается в строку и сохраняется в локальную переменную.