Из Википедии, бесплатной энциклопедии
Перейти к навигации Перейти к поиску

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

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

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

Иерархия и классификация [ править ]

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

  • Тип переменной является безусловным идентификатором. Переменные типа вводятся объявлениями универсального класса, объявлениями универсального интерфейса, объявлениями универсального метода и объявлениями универсального конструктора.
  • Класс является общим , если он объявляет одну или несколько переменных типа. Эти переменные типа известны как параметры типа класса. Он определяет одну или несколько переменных типа, которые действуют как параметры. Объявление универсального класса определяет набор параметризованных типов, по одному для каждого возможного вызова раздела параметров типа. Все эти параметризованные типы используют один и тот же класс во время выполнения.
  • Интерфейс является общим , если он объявляет одну или несколько переменных типа. Эти переменные типа известны как параметры типа интерфейса. Он определяет одну или несколько переменных типа, которые действуют как параметры. Объявление универсального интерфейса определяет набор типов, по одному для каждого возможного вызова раздела параметров типа. Все параметризованные типы используют один и тот же интерфейс во время выполнения.
  • Метод является общим , если он объявляет одну или несколько переменных типа. Эти переменные типа известны как параметры формального типа метода. Форма списка параметров формального типа идентична списку параметров типа класса или интерфейса.
  • Конструктор может быть объявлен как родовые, независимо от того , класса , что конструктор объявлен в сам родовом. Конструктор является универсальным, если он объявляет одну или несколько переменных типа. Эти переменные типа известны как параметры формального типа конструктора. Форма списка параметров формального типа идентична списку параметров типа универсального класса или интерфейса.

Мотивация [ править ]

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

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

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

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

Список < String >  v  =  новый  ArrayList < String > (); v . добавить ( "тест" ); Целое число  i  =  ( Целое число ) v . получить ( 0 );  // (ошибка типа) ошибка времени компиляции

Параметр типа Stringв угловых скобках объявляет, что объект ArrayListсостоит из String(потомка ArrayListобщих Objectкомпонентов). С универсальными шаблонами больше нет необходимости приводить третью строку к какому-либо конкретному типу, потому что результат v.get(0)определяется Stringкодом, сгенерированным компилятором.

Логическая ошибка в третьей строке этого фрагмента будет обнаружена как ошибка времени компиляции (с J2SE 5.0 или новее), потому что компилятор обнаружит, что v.get(0)возвращается Stringвместо Integer. Более подробный пример см. В ссылке. [5]

Вот небольшой отрывок из определения интерфейсов Listи Iteratorпакета java.util:

публичный  интерфейс  List < E >  {  void  add ( E  x );  Итератор < E >  iterator (); }открытый  интерфейс  Iterator < E >  {  E  next ();  логическое  hasNext (); }

Подстановочные знаки типа [ править ]

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

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

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

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

Чтобы указать верхнюю границу подстановочного знака типа, extendsключевое слово используется, чтобы указать, что аргумент типа является подтипом ограничивающего класса. Значит List<? extends Number>, данный список содержит объекты неизвестного типа, расширяющие Numberкласс. Например, список может быть List<Float>или List<Number>. Чтение элемента из списка вернет Number. Также разрешено добавление нулевых элементов. [7]

Использование подстановочных знаков выше добавляет гибкости, поскольку между любыми двумя параметризованными типами с конкретным типом в качестве аргумента типа отсутствует какое-либо отношение наследования. Ни то, List<Number>ни другое не List<Integer>является подтипом другого; хотя Integerэто подтип Number. Таким образом, любой метод, который принимает List<Number>в качестве параметра, не принимает аргумент List<Integer>. Если бы это было так, то можно было бы вставить в Numberнего не a Integer; что нарушает безопасность типов. Вот пример, демонстрирующий, как безопасность типов была бы нарушена, если бы List<Integer>был подтип List<Number>:

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

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

Список <?  расширяет  Number >  nums  =  ints ;  // ОК nums . добавить ( 3.14 );  // ошибка времени компиляции НУМС . добавить ( ноль );  // разрешается

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

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

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

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

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

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

Entry < String ,  String >  grade  =  new  Entry < String ,  String > ( «Майк» ,  «A» ); Запись < String ,  Integer >  mark  =  new  Entry < String ,  Integer > ( "Майк" ,  100 ); Система . из . println ( "оценка:"  +  оценка ); Система .вне. println ( "отметка:"  +  отметка );Entry < Integer ,  Boolean >  prime  =  new  Entry < Integer ,  Boolean > ( 13 ,  истина ); если  ( простое . getValue ())  System . из . println ( простое число . getKey ()  +  "простое число" ); else  System . из . println ( простое . getKey ()  + "не простое". );

Он выводит:

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

Алмазный оператор [ править ]

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

Запись < String ,  String >  grade  =  new  Entry <> ( «Майк» ,  «А» ); Запись < String ,  Integer >  mark  =  new  Entry <> ( "Майк" ,  100 ); Система . из . println ( "оценка:"  +  оценка ); Система . из . println ( "отметка:"  +  отметка );Entry < Integer ,  Boolean >  prime  =  new  Entry <> ( 13 ,  истина ); если  ( простое . getValue ())  System . из . println ( простое число . getKey ()  +  "простое число" ); else  System . из . println ( prime . getKey ()  +  "не является простым." );

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

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

общедоступный  статический  < Тип >  Запись < Тип ,  Тип >  дважды ( значение типа  ) { возвращает новую запись < Тип , Тип > ( значение , значение ); }      

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

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

Запись < Строка ,  Строка >  пара  =  Запись . дважды ( «Привет» );

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

Запись < Строка ,  Строка >  пара  =  Запись . < Строка > дважды ( "Привет" );

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

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

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

общедоступный  < Тип >  Тип []  toArray ( Тип ...  элементы )  {  возвращаемые  элементы ; }

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

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

Дженерики в предложении throws [ править ]

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

общественное  < Т  расширяет  Throwable >  недействительный  throwMeConditional ( булевы  условный ,  Т  исключения )  бросает  T  {  если  ( условный )  {  бросок  исключение ;  } }

Проблемы со стиранием типа [ править ]

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

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

Демонстрируя это, следующий код выводит "Equal":

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

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

открытый  класс  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 )  {  вернуть  новый  T ();  // вызывает ошибку компиляции }

Поскольку во время выполнения существует только одна копия на универсальный класс, статические переменные используются всеми экземплярами класса независимо от их параметра типа. Следовательно, параметр типа нельзя использовать в объявлении статических переменных или в статических методах.

Проект по дженерикам [ править ]

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

  • общая специализация , например List <int>
  • овеществленные дженерики ; предоставление фактических типов во время выполнения.

См. Также [ править ]

  • Общее программирование
  • Метапрограммирование шаблона
  • Подстановочный знак (Java)
  • Сравнение C # и Java
  • Сравнение Java и C ++

Ссылки [ править ]

  1. ^ Язык программирования Java
  2. ^ Исключение ClassCastException может быть сгенерировано даже при отсутствии приведения типов или значений NULL. «Системы типов Java и Scala не верны» (PDF) .
  3. ^ GJ: Generic Java
  4. ^ Спецификация языка Java, третье издание Джеймса Гослинга, Билла Джоя, Гая Стила, Гилада Браха - Prentice Hall PTR 2005
  5. Гилад Браха (5 июля 2004 г.). «Обобщения на языке программирования Java» (PDF) . www.oracle.com .
  6. Гилад Браха (5 июля 2004 г.). «Обобщения на языке программирования Java» (PDF) . www.oracle.com . п. 5.
  7. ^ Браха, Гилад . «Подстановочные знаки> Бонус> Дженерики» . Учебники по Java ™ . Oracle. ... Единственное исключение - null, который является членом любого типа ...
  8. ^ http://docs.oracle.com/javase/7/docs/technotes/guides/language/type-inference-generic-instance-creation.html
  9. ^ Gafter, Нил (2006-11-05). «Reified Generics для Java» . Проверено 20 апреля 2010 .
  10. ^ «Спецификация языка Java, раздел 8.1.2» . Oracle . Проверено 24 октября 2015 года .
  11. ^ Гетц, Брайан. "Добро пожаловать в Валгаллу!" . Почтовый архив OpenJDK . OpenJDK . Проверено 12 августа 2014 .