Generics — это средство обобщенного программирования , которое было добавлено в язык программирования Java в 2004 году в версии J2SE 5.0. Они были разработаны для расширения системы типов Java , чтобы позволить «типу или методу работать с объектами различных типов, обеспечивая при этом безопасность типов во время компиляции». [1] Аспект безопасности типов во время компиляции требовал, чтобы параметрически полиморфные функции не были реализованы в виртуальной машине Java , поскольку безопасность типов в этом случае невозможна. [2] [3]
Фреймворк коллекций Java поддерживает универсальные типы для указания типа объектов, хранящихся в экземпляре коллекции.
В 1998 году Гилад Браха , Мартин Одерски , Дэвид Стаутэмайр и Филип Вадлер создали Generic Java — расширение языка Java для поддержки универсальных типов. [4] Generic Java был включён в Java с добавлением подстановочных знаков .
Согласно спецификации языка Java : [5]
Следующий блок кода Java иллюстрирует проблему, которая существует, когда не используются обобщенные типы. Сначала он объявляет тип ArrayList
. Object
Затем он добавляет String
к ArrayList
. Наконец, он пытается извлечь добавленный String
и привести его к Integer
— ошибка в логике, так как в общем случае невозможно привести произвольную строку к целому числу.
final List v = new ArrayList (); v . add ( "test" ); // Строка, которая не может быть преобразована в целое число final Integer i = ( Integer ) v . get ( 0 ); // Ошибка времени выполнения
Хотя код скомпилирован без ошибок, он выдает исключение времени выполнения ( java.lang.ClassCastException
) при выполнении третьей строки кода. Этот тип логической ошибки может быть обнаружен во время компиляции с помощью generics [7] и является основной мотивацией для их использования. [6] Он определяет одну или несколько переменных типа, которые действуют как параметры.
Приведенный выше фрагмент кода можно переписать с использованием дженериков следующим образом:
final List < String > v = new ArrayList < String > (); v . add ( "test" ); final Integer i = ( Integer ) v . get ( 0 ); // (ошибка типа) ошибка времени компиляции
Параметр типа String
в угловых скобках объявляет, что ArrayList
он состоит из String
(потомка ArrayList
универсальных Object
компонентов ). С универсальными типами больше нет необходимости приводить третью строку к какому-либо конкретному типу, поскольку результат v.get(0)
определяется как String
код, сгенерированный компилятором.
Логическая ошибка в третьей строке этого фрагмента будет обнаружена как ошибка времени компиляции (с J2SE 5.0 или более поздней версией), поскольку компилятор обнаружит, что v.get(0)
возвращается String
вместо Integer
. [7] Более подробный пример см. в ссылке. [9]
Вот небольшая выдержка из определения интерфейсов java.util.List
и java.util.Iterator
пакета java.util
:
Список интерфейсов < E > { void add ( E x ); Итератор < E > итератор (); } Итератор интерфейса < E > { E следующий (); логическое значение hasNext (); }
Вот пример универсального класса Java, который можно использовать для представления отдельных записей (сопоставлений ключей и значений) в карте :
public class Entry < KeyType , ValueType > { private final KeyType key ; private final ValueType value ; public Entry ( KeyType key , ValueType value ) { this.key = key ; this.value = value ; } public KeyType getKey () { return key ; } public ValueType getValue () { возвращаемое значение ; } public String toString () { return "(" + key + ", " + value + ")" ; } }
Этот универсальный класс можно использовать, например, следующими способами:
final Entry < String , String > оценка = new Entry < String , String > ( "Майк" , "A" ); final Entry < String , Integer > отметка = new Entry < String , Integer > ( "Майк" , 100 ); System . out . println ( "оценка: " + оценка ); System . out . println ( "отметка: " + отметка ); final Entry < Integer , Boolean > prime = new Entry < Integer , Boolean > ( 13 , true ); if ( prime . getValue ()) { System . out . println ( prime . getKey () + " является простым числом." ); } else { System . out . println ( prime . getKey () + " не является простым числом." ); }
Выводит:
оценка: (Майк, A)оценка: (Майк, 100)13 — простое число.
Вот пример универсального метода, использующего указанный выше универсальный класс:
public static < Тип > Entry < Тип , Тип > дважды ( Тип значение ) { return new Entry < Тип , Тип > ( значение , значение ); }
Примечание: Если мы удалим первый элемент <Type>
в приведенном выше методе, мы получим ошибку компиляции (не удается найти символ «Тип»), поскольку он представляет собой объявление символа.
Во многих случаях пользователю метода не нужно указывать параметры типа, поскольку они могут быть выведены:
окончательная запись < String , String > pair = Entry . twice ( "Привет" );
При необходимости параметры можно явно добавить:
окончательная запись < Строка , Строка > пара = Запись . < Строка > дважды ( "Привет" );
Использование примитивных типов не допускается, вместо этого следует использовать коробочные версии:
final Entry < int , int > pair ; // Не удается выполнить компиляцию. Вместо этого используйте Integer.
Также существует возможность создания универсальных методов на основе заданных параметров.
public < Type > Type [] toArray ( Type ... elements ) { return elements ; }
В таких случаях также нельзя использовать примитивные типы, например:
Целое число [] массив = toArray ( 1 , 2 , 3 , 4 , 5 , 6 );
Благодаря выводу типов Java SE 7 и выше позволяет программисту заменять пустую пару угловых скобок ( <>
, называемую оператором «ромб» ) на пару угловых скобок, содержащих один или несколько параметров типа, которые подразумевает достаточно близкий контекст . [10] Таким образом, приведенный выше пример кода с использованием Entry
можно переписать как:
final Entry < String , String > оценка = new Entry <> ( "Майк" , "A" ); final Entry < String , Integer > отметка = new Entry <> ( "Майк" , 100 ); System . out . println ( "оценка: " + оценка ); System . out . println ( "оценка: " + отметка ); final Entry < Integer , Boolean > prime = new Entry <> ( 13 , true ); if ( prime . getValue ()) System . out . println ( prime . getKey () + " является простым числом." ); else System . out . println ( prime . getKey () + " не является простым числом." );
Аргумент типа для параметризованного типа не ограничивается конкретным классом или интерфейсом. Java позволяет использовать «подстановочные знаки типа» в качестве аргументов типа для параметризованных типов. Подстановочные знаки — это аргументы типа в форме « <?>
»; необязательно с верхней или нижней границей . Учитывая, что точный тип, представленный подстановочным знаком, неизвестен, на тип методов, которые могут быть вызваны для объекта, использующего параметризованные типы, накладываются ограничения.
Вот пример, в котором тип элемента a Collection<E>
параметризуется подстановочным знаком:
final Collection <?> c = new ArrayList < String > (); c . add ( new Object ()); // ошибка времени компиляции c . add ( null ); // разрешено
c
Поскольку мы не знаем, что означает тип элемента , мы не можем добавлять к нему объекты. add()
Метод принимает аргументы типа E
, типа элемента Collection<E>
универсального интерфейса. Когда фактический аргумент типа — ?
, он означает некий неизвестный тип. Любое значение аргумента метода, которое мы передаем методу add()
, должно быть подтипом этого неизвестного типа. Поскольку мы не знаем, какой это тип, мы не можем ничего передать. Единственным исключением является null ; который является членом каждого типа. [11]
Чтобы указать верхнюю границу подстановочного знака типа, extends
используется ключевое слово, указывающее, что аргумент типа является подтипом ограничивающего класса. [12] So означает, что данный список содержит объекты некоторого неизвестного типа, который расширяет класс. Например, список может быть или . Чтение элемента из списка вернет . Добавление нулевых элементов, опять же, также разрешено. [13]List<? extends Number>
Number
List<Float>
List<Number>
Number
Использование подстановочных знаков выше добавляет гибкости [12], поскольку нет никаких отношений наследования между любыми двумя параметризованными типами с конкретным типом в качестве аргумента типа. Ни один из них List<Number>
не List<Integer>
является подтипом другого; даже если Integer
является подтипом Number
. [12] Таким образом, любой метод, который принимает List<Number>
в качестве параметра , не принимает аргумент List<Integer>
. Если бы это было так, можно было бы вставить в него Number
, который не является Integer
; что нарушает безопасность типов. Вот пример, демонстрирующий, как безопасность типов была бы нарушена, если List<Integer>
бы был подтипом List<Number>
:
final List < Integer > ints = new ArrayList <> (); ints . add ( 2 ); final List < Number > nums = ints ; // действительно, если List<Integer> был подтипом List<Number> согласно правилу подстановки. nums . add ( 3.14 ); final Integer x = ints . get ( 1 ); // теперь 3.14 присвоено переменной Integer!
Решение с использованием подстановочных знаков работает, поскольку оно запрещает операции, которые нарушают безопасность типов:
final List <? extends Number > nums = ints ; // OK nums . add ( 3.14 ); // ошибка времени компиляции nums . add ( null ); // разрешено
Чтобы указать нижний ограничивающий класс подстановочного знака типа, super
используется ключевое слово. Это ключевое слово указывает, что аргумент типа является супертипом ограничивающего класса. Таким образом, может представлять или . Чтение из списка, определенного как , возвращает элементы типа . Добавление в такой список требует либо элементов типа , либо любого подтипа , либо null (который является членом каждого типа).List<? super Number>
List<Number>
List<Object>
List<? super Number>
Object
Number
Number
Мнемоническая аббревиатура PECS (Producer Extends, Consumer Super) из книги « Эффективная Java» Джошуа Блоха дает простой способ запомнить, когда следует использовать подстановочные знаки (соответствующие ковариантности и контравариантности ) в Java. [12]
Хотя сами исключения не могут быть универсальными, универсальные параметры могут появляться в предложении throws:
public < T extends Throwable > void throwMeConditional ( булевское условное выражение , исключение T ) throws T { if ( conditional ) { throw exception ; } }
Generics проверяются во время компиляции на корректность типа. [7] Затем информация об универсальном типе удаляется в процессе, называемом стиранием типа . [6] Например, List<Integer>
будет преобразован в неуниверсальный тип List
, который обычно содержит произвольные объекты. Проверка во время компиляции гарантирует, что полученный код использует правильный тип. [7]
Из-за стирания типа параметры типа не могут быть определены во время выполнения. [6] Например, когда an ArrayList
проверяется во время выполнения, нет общего способа определить, был ли он до стирания типа ArrayList<Integer>
или ArrayList<Float>
. Многие люди недовольны этим ограничением. [14] Существуют частичные подходы. Например, отдельные элементы могут быть проверены для определения типа, к которому они принадлежат; например, если an ArrayList
содержит Integer
, то ArrayList мог быть параметризован с помощью Integer
(однако он мог быть параметризован с помощью любого родителя Integer
, например Number
или Object
).
Демонстрируя этот момент, следующий код выводит «Равно»:
final List < Integer > li = new ArrayList <> (); final List < Float > lf = new ArrayList <> (); if ( li . getClass () == lf . getClass ()) { // возвращает значение true System . out . println ( "Equal" ); }
Другим эффектом стирания типа является то, что универсальный класс не может расширять Throwable
класс каким-либо образом, напрямую или косвенно: [15]
открытый класс GenericException < T > расширяет Exception
Причина, по которой это не поддерживается, заключается в стирании типа:
try { throw new GenericException < Integer > ( ) ; } catch ( GenericException < Integer > e ) { System.err.println ( " Integer " ) ; } catch ( GenericException < String > e ) { System.err.println ( " String " ) ; }
Из-за стирания типов среда выполнения не будет знать, какой блок catch выполнять, поэтому это запрещено компилятором.
Обобщенные типы Java отличаются от шаблонов C++ . Обобщенные типы Java генерируют только одну скомпилированную версию обобщенного класса или функции независимо от количества используемых параметризующих типов. Более того, среде выполнения Java не нужно знать, какой параметризованный тип используется, поскольку информация о типе проверяется во время компиляции и не включается в скомпилированный код. Следовательно, создание экземпляра класса Java параметризованного типа невозможно, поскольку создание экземпляра требует вызова конструктора, который недоступен, если тип неизвестен.
Например, следующий код не может быть скомпилирован:
< T > T instantiateElementType ( List < T > arg ) { return new T (); //вызывает ошибку компиляции }
Поскольку во время выполнения существует только одна копия на общий класс, статические переменные являются общими для всех экземпляров класса, независимо от их параметра типа. Следовательно, параметр типа не может использоваться в объявлении статических переменных или в статических методах.
Стирание типов было реализовано в Java для поддержания обратной совместимости с программами, написанными до Java SE5. [7]
Есть несколько важных различий между массивами (как примитивными массивами, так и Object
массивами) и дженериками в Java. Два из основных различий, а именно, различия в терминах дисперсии и конкретизации .
Универсальные объекты инвариантны, тогда как массивы ковариантны . [6] Это преимущество использования универсальных объектов по сравнению с неуниверсальными объектами, такими как массивы. [6] В частности, универсальные объекты могут помочь предотвратить исключения во время выполнения, выдавая исключение во время компиляции, чтобы заставить разработчика исправить код.
Например, если разработчик объявляет Object[]
объект и создает экземпляр объекта как новый Long[]
объект, исключение времени компиляции не выбрасывается (поскольку массивы ковариантны). [6] Это может создать ложное впечатление, что код написан правильно. Однако, если разработчик попытается добавить String
к этому Long[]
объекту, программа выдаст ArrayStoreException
. [6] Этого исключения времени выполнения можно полностью избежать, если разработчик использует обобщенные типы.
Если разработчик объявляет Collection<Object>
объект и создает новый экземпляр этого объекта с возвращаемым типом ArrayList<Long>
, компилятор Java (правильно) выдаст исключение времени компиляции, чтобы указать на наличие несовместимых типов (поскольку обобщенные типы инвариантны). [6] Следовательно, это позволяет избежать потенциальных исключений времени выполнения. Эту проблему можно исправить, создав вместо этого экземпляр Collection<Object>
using ArrayList<Object>
object. Для кода, использующего Java SE7 или более поздние версии, Collection<Object>
можно создать экземпляр с ArrayList<>
объектом, используя оператор diamond
Массивы являются овеществленными , то есть объект массива обеспечивает свою информацию о типе во время выполнения, тогда как обобщенные типы в Java не являются овеществленными. [6]
Говоря более формально, объекты с универсальным типом в Java являются нереифицируемыми типами. [6] Нереифицируемый тип — это тип, представление которого во время выполнения содержит меньше информации, чем его представление во время компиляции. [6]
Объекты с универсальным типом в Java не могут быть повторно определены из-за стирания типа. [6] Java обеспечивает информацию о типе только во время компиляции. После проверки информации о типе во время компиляции информация о типе отбрасывается, и во время выполнения информация о типе не будет доступна. [6]
Примерами нереифицируемых типов являются List<T>
и List<String>
, где T
— общий формальный параметр. [6]
Проект Valhalla — это экспериментальный проект по инкубации улучшенных дженериков Java и языковых функций для будущих версий, потенциально начиная с Java 10. Потенциальные улучшения включают: [16]
...Единственным исключением является null, который является членом каждого типа...