stringtranslate.com

Дженерики в Java

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>NumberList<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>ObjectNumberNumber

Мнемоническая аббревиатура PECS (Producer Extends, Consumer Super) из книги « Эффективная Java» Джошуа Блоха дает простой способ запомнить, когда следует использовать подстановочные знаки (соответствующие ковариантности и контравариантности ) в Java. [12]

Обобщения в пункте throws

Хотя сами исключения не могут быть универсальными, универсальные параметры могут появляться в предложении 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]

Смотрите также

Цитаты

  1. ^ Язык программирования Java
  2. ^ ClassCastException может быть выброшено даже при отсутствии приведения типов или значений null. «Системы типов Java и Scala ненадежны» (PDF) .
  3. ^ Bloch 2018, стр. 123–125, Глава §5 Пункт 27: Устранение непроверенных предупреждений.
  4. ^ GJ: Универсальный Java
  5. ^ Спецификация языка Java, третье издание Джеймса Гослинга, Билла Джоя, Гая Стила, Гилада Брахи – Prentice Hall PTR 2005
  6. ^ abcdefghijklmno Bloch 2018, стр. 126–129, Глава §5 Пункт 28: Предпочитайте списки массивам.
  7. ^ abcdefgh Bloch 2018, стр. 117–122, Глава §5 Пункт 26: Не используйте необработанные типы.
  8. ^ Bloch 2018, стр. 135–138, Глава §5 Пункт 30: Отдавайте предпочтение универсальным методам.
  9. ^ Гилад Браха (5 июля 2004 г.). «Дженерики в языке программирования Java» (PDF) . www.oracle.com .
  10. ^ «Вывод типа для создания универсального экземпляра».
  11. ^ Гилад Браха (5 июля 2004 г.). «Дженериксы в языке программирования Java» (PDF) . www.oracle.com . стр. 5.
  12. ^ abcd Bloch 2018, стр. 139–145, Глава §5, пункт 31: Используйте ограниченные подстановочные знаки для повышения гибкости API.
  13. ^ Браха, Гилад . "Шаблоны > Бонус > Универсальные". Учебники Java™ . Oracle. ...Единственным исключением является null, который является членом каждого типа...
  14. ^ Гафтер, Нил (2006-11-05). "Reified Generics for Java" . Получено 20-04-2010 .
  15. ^ "Спецификация языка Java, раздел 8.1.2". Oracle . Получено 24 октября 2015 г. .
  16. ^ Гетц, Брайан. «Добро пожаловать в Вальхаллу!». Архив почты OpenJDK . OpenJDK . Получено 12 августа 2014 г.

Ссылки