stringtranslate.com

Дженерики в Java

Дженерики — это средства универсального программирования , которые были добавлены в язык программирования Java в 2004 году в версии J2SE 5.0. Они были разработаны для расширения системы типов Java , чтобы позволить «типу или методу работать с объектами различных типов, обеспечивая при этом безопасность типов во время компиляции». [1] Безопасность типов во время компиляции аспекта не была полностью достигнута, поскольку в 2016 году было показано, что она не гарантируется во всех случаях. [2] [3]

Платформа коллекций Java поддерживает универсальные шаблоны для указания типа объектов, хранящихся в экземпляре коллекции.

В 1998 году Гилад Брача , Мартин Одерски , Дэвид Стаутамир и Филип Уодлер создали Generic Java — расширение языка Java для поддержки универсальных типов. [4] Generic Java был включен в Java с добавлением подстановочных знаков .

Иерархия и классификация

Согласно спецификации языка Java : [5]

Мотивация

Следующий блок кода Java иллюстрирует проблему, которая возникает, когда не используются дженерики. Во-первых, он объявляет ArrayListтип Object. Затем он добавляет Stringк ArrayList. Наконец, он пытается получить добавленное значение Stringи привести его к Integer— логической ошибке, поскольку обычно невозможно привести произвольную строку к целому числу.

окончательный список v = новый ArrayList (); в . добавить ( «тест» ); // Строка, которую нельзя привести к целому числу Final Integer i = ( Integer ) v . получить ( 0 ); // Ошибка выполнения            

Хотя код компилируется без ошибок, java.lang.ClassCastExceptionпри выполнении третьей строки кода выдается исключение времени выполнения ( ). Этот тип логической ошибки может быть обнаружен во время компиляции с помощью дженериков [7] и является основной мотивацией для их использования. [6] Он определяет одну или несколько переменных типа, которые действуют как параметры.

Приведенный выше фрагмент кода можно переписать с использованием дженериков следующим образом:

окончательный список <String> v = новый ArrayList <String> ( ) ; _ _ в . добавить ( «тест» ); последнее целое число i = ( Целое число ) v . получить ( 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> { _ _  аннулировать добавить ( Е х ); Итератор <E> итератор ( ) ;    } Итератор интерфейса <E> { _ _  Е следующий (); логическое значение hasNext ();   }

Определения общих классов

Вот пример общего класса Java, который можно использовать для представления отдельных записей (сопоставлений ключей и значений) на карте :

общедоступный класс Entry < KeyType , ValueType > { частный окончательный ключ KeyType ; частное окончательное значение ValueType ;              публичная запись ( ключ KeyType , значение ValueType ) { this . ключ = ключ ; этот . значение = значение ; }              public KeyType getKey () { ключ возврата ; }       public ValueType getValue () { возвращаемое значение ; }       public String toString () { return "(" + key + ", " + value + ")" ; }                }

Этот универсальный класс можно использовать, например, следующими способами:

окончательная запись < String , String > оценка = новая запись < String , String > ( «Майк» , «A» ); окончательная запись < String , Integer > mark = новая запись < String , Integer > ( «Майк» , 100 ); Система . вне . println ( "оценка:" + оценка ); Система . вне . println ( "отметить:" + отметить );                    окончательная запись < целое число , логическое значение > prime = новая запись < целое число , логическое значение > ( 13 , true ); если ( prime . getValue ()) { System . вне . println ( prime . getKey () + «является простым.» ); } Еще { Система . вне . println ( prime . getKey () + «не является простым.» ); }                 

Он выводит:

оценка: (Майк, А)отметка: (Майк, 100)13 — простое число.

Определения общих методов

Вот пример универсального метода, использующего приведенный выше универсальный класс:

public static <Type> Entry < Type , Type > дважды ( Type value ) { return new Entry < Type , Type > ( value , value ) ; _ }            

Примечание. Если мы удалим первый <Type>из приведенного выше метода, мы получим ошибку компиляции (невозможно найти символ «Тип»), поскольку он представляет собой объявление символа.

Во многих случаях пользователю метода не нужно указывать параметры типа, поскольку они могут быть выведены следующим образом:

окончательная запись < строка , строка > пара = запись . дважды ( «Привет» );     

При необходимости параметры можно добавить явно:

окончательная запись < строка , строка > пара = запись . < String > дважды ( «Привет» );     

Использование примитивных типов не допускается, вместо них необходимо использовать коробочные версии:

окончательная запись < int , int > пара ; // Ошибка компиляции. Вместо этого используйте целое число.    

Существует также возможность создавать универсальные методы на основе заданных параметров.

public <Type> Type [ ] toArray ( Type ... elements ) { return elements ; _ }       

В таких случаях вы также не можете использовать примитивные типы, например:

Целочисленный [] массив = toArray ( 1 , 2 , 3 , 4 , 5 , 6 );        

Алмазный оператор

Благодаря выводу типа Java SE 7 и выше позволяет программисту заменять пустую пару угловых скобок ( <>называемых оператором ромба ) парой угловых скобок, содержащих один или несколько параметров типа, которые подразумевает достаточно близкий контекст . [10] Таким образом, приведенный выше пример кода Entryможно переписать как:

окончательная запись < String , String > оценка = новая запись <> ( «Майк» , «A» ); окончательная запись < String , Integer > mark = новая запись <> ( «Майк» , 100 ); Система . вне . println ( "оценка:" + оценка ); Система . вне . println ( "отметить:" + отметить );                  окончательная запись < целое число , логическое значение > prime = новая запись <> ( 13 , true ); if ( prime . getValue ()) System . вне . println ( prime . getKey () + «является простым.» ); еще Система . вне . println ( prime . getKey () + «не является простым.» );              

Введите подстановочные знаки

Аргумент типа для параметризованного типа не ограничивается конкретным классом или интерфейсом. Java позволяет использовать «подстановочные знаки типов» в качестве аргументов типа для параметризованных типов. Подстановочные знаки — это аргументы типа в форме " <?>"; опционально с верхней или нижней границей . Поскольку точный тип, представленный подстановочным знаком, неизвестен, накладываются ограничения на тип методов, которые можно вызывать для объекта, использующего параметризованные типы.

Вот пример, где тип элемента a Collection<E>параметризуется подстановочным знаком:

окончательная коллекция <?> c = новый ArrayList <String> ( ) ; в . добавить ( новый объект ()); // ошибка времени компиляции c . добавить ( ноль ); // допустимый        

Поскольку мы не знаем, что cозначает тип элемента, мы не можем добавлять к нему объекты. Метод add()принимает аргументы типа E— типа элемента Collection<E>универсального интерфейса. Когда фактический аргумент типа равен ?, он обозначает какой-то неизвестный тип. Любое значение аргумента метода, которое мы передаем этому add()методу, должно быть подтипом этого неизвестного типа. Поскольку мы не знаем, что это за тип, мы не можем ничего передать. Единственное исключение — null ; который является членом каждого типа. [11]

Чтобы указать верхнюю границу подстановочного знака типа, extendsиспользуется ключевое слово, указывающее, что аргумент типа является подтипом ограничивающего класса. [12] Это означает, что данный список содержит объекты некоторого неизвестного типа, который расширяет класс. Например, список может быть или . Чтение элемента из списка вернет . Добавление нулевых элементов также разрешено. [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>:

окончательный список < целое число > ints = новый ArrayList <> (); целые числа . добавить ( 2 ); окончательный список < номер > nums = ints ; // допустимо, если List<Integer> был подтипом List<Number> в соответствии с правилом подстановки. цифры . добавить ( 3.14 ); окончательное целое число x = ints . получить ( 1 ); // теперь 3.14 присвоено целочисленной переменной!                

Решение с подстановочными знаками работает, поскольку оно запрещает операции, нарушающие типобезопасность:

окончательный список <? расширяет число > nums = ints ; // ОК, числа . добавить ( 3.14 ); // номера ошибок времени компиляции . добавить ( ноль ); // допустимый         

Чтобы указать нижний ограничивающий класс подстановочного знака типа, superиспользуется ключевое слово. Это ключевое слово указывает, что аргумент типа является супертипом ограничивающего класса. Итак, может представлять собой или . Чтение из списка, определенного как, возвращает элементы типа . Для добавления в такой список требуются элементы type , любого подтипа или null (который является членом каждого типа).List<? super Number>List<Number>List<Object>List<? super Number>ObjectNumberNumber

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

Обобщения в предложении throws

Хотя сами по себе исключения не могут быть универсальными, в предложении throws могут присутствовать универсальные параметры:

public < T расширяет Throwable > void throwMeConditional ( логическое условие , исключение T ) выдает T { if ( условное ) { исключение исключения ; } }                 

Проблемы со стиранием типа

Обобщенные типы проверяются во время компиляции на правильность типов. [7] Информация об общем типе затем удаляется в процессе, называемом стиранием типа . [6] Например, List<Integer>будет преобразован в необобщенный тип List, который обычно содержит произвольные объекты. Проверка во время компиляции гарантирует, что результирующий код использует правильный тип. [7]

Из-за стирания типа параметры типа не могут быть определены во время выполнения. [6] Например, когда объект ArrayListпроверяется во время выполнения, не существует общего способа определить, был ли он перед стиранием типа типом ArrayList<Integer>или типом ArrayList<Float>. Многие люди недовольны этим ограничением. [14] Существуют частичные подходы. Например, можно проверить отдельные элементы, чтобы определить тип, к которому они принадлежат; например, если an ArrayListсодержит Integer, этот ArrayList мог быть параметризован с помощью Integer(однако он мог быть параметризован с помощью любого родительского элемента Integer, например Numberили Object).

Демонстрируя это, следующий код выводит «Равно»:

окончательный список < целое число > li = новый ArrayList <> (); окончательный список <Float> lf = новый ArrayList <> ( ) ; if ( li . getClass () == lf . getClass ()) { // оценивается как true System . вне . println ( "Равно" ); }                

Другой эффект стирания типа заключается в том, что обобщенный класс не может Throwableкаким-либо образом расширять класс, прямо или косвенно: [15]

общедоступный класс GenericException <T> расширяет исключение _ _    

Причина, по которой это не поддерживается, связана со стиранием типа:

попробуйте { бросить новое GenericException <Integer> ( ) ; } catch ( GenericException <Integer> e ) { System . _ _ ошибаюсь . println ( "Целое число" ); } catch ( GenericException <String> e ) { System . _ _ ошибаюсь . println ( "Строка" ); }            

Из-за стирания типа среда выполнения не будет знать, какой блок 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>использования ArrayList<Object>объекта. Для кода, использующего Java SE7 или более поздние версии, Collection<Object>можно создать экземпляр ArrayList<>объекта с помощью оператора ромба.

реификация

Массивы материализованы , что означает, что объект массива применяет информацию о своем типе во время выполнения, тогда как дженерики в Java не материализуются. [6]

Более формально говоря, объекты универсального типа в Java являются нереифицируемыми типами. [6] Неповторяемый тип — это тип, представление которого во время выполнения содержит меньше информации, чем его представление во время компиляции. [6]

Объекты универсального типа в Java не подлежат повторному использованию из-за стирания типа. [6] Java применяет информацию о типе только во время компиляции. После того как информация о типе проверена во время компиляции, информация о типе отбрасывается, а во время выполнения информация о типе не будет доступна. [6]

Примеры неповторяемых типов включают List<T>и List<String>, где T— общий формальный параметр. [6]

Проект по дженерикам

Project Valhalla — это экспериментальный проект по внедрению улучшенных дженериков и функций языка Java для будущих версий, возможно, начиная с Java 10. Потенциальные улучшения включают в себя: [16]

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

Цитаты

  1. ^ Язык программирования Java
  2. ^ Исключение ClassCastException может быть выброшено даже при отсутствии приведения или нулевых значений. «Системы типов 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™ . Оракул. ...Единственным исключением является значение null, которое является членом каждого типа...
  14. ^ Гафтер, Нил (5 ноября 2006 г.). «Reified Generics для Java» . Проверено 20 апреля 2010 г.
  15. ^ «Спецификация языка Java, раздел 8.1.2» . Оракул . Проверено 24 октября 2015 г.
  16. ^ Гетц, Брайан. «Добро пожаловать в Валгаллу!». Почтовый архив OpenJDK . OpenJDK . Проверено 12 августа 2014 г.

Рекомендации