Jump to content

Дженерики в Java

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

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

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

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

[ редактировать ]

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

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

Мотивация

[ редактировать ]

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

final List v = new ArrayList();
v.add("test"); // A String that cannot be cast to an Integer
final Integer i = (Integer) v.get(0); // Run time error

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

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

final List<String> v = new ArrayList<String>();
v.add("test");
final Integer i = (Integer) v.get(0); // (type error)  compilation-time error

Параметр типа 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:

interface List<E> {

    void add(E x);
    Iterator<E> iterator();

}

interface Iterator<E> {

    E next();
    boolean 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() {
        return value;
    }

    public String toString() { 
        return "(" + key + ", " + value + ")";  
    }

}

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

final Entry<String, String> grade = new Entry<String, String>("Mike", "A");
final Entry<String, Integer> mark = new Entry<String, Integer>("Mike", 100);
System.out.println("grade: " + grade);
System.out.println("mark: " + mark);

final Entry<Integer, Boolean> prime = new Entry<Integer, Boolean>(13, true);
if (prime.getValue()) {
    System.out.println(prime.getKey() + " is prime.");
}
else {
    System.out.println(prime.getKey() + " is not prime.");
}

Он выводит:

grade: (Mike, A)
mark: (Mike, 100)
13 is prime.

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

[ редактировать ]

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

public static <Type> Entry<Type, Type> twice(Type value) {
    return new Entry<Type, Type>(value, value);
}

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

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

final Entry<String, String> pair = Entry.twice("Hello");

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

final Entry<String, String> pair = Entry.<String>twice("Hello");

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

final Entry<int, int> pair; // Fails compilation. Use Integer instead.

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

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

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

Integer[] array = toArray(1, 2, 3, 4, 5, 6);

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

[ редактировать ]

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

final Entry<String, String> grade = new Entry<>("Mike", "A");
final Entry<String, Integer> mark = new Entry<>("Mike", 100);
System.out.println("grade: " + grade);
System.out.println("mark: " + mark);

final Entry<Integer, Boolean> prime = new Entry<>(13, true);
if (prime.getValue()) System.out.println(prime.getKey() + " is prime.");
else System.out.println(prime.getKey() + " is not prime.");

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

[ редактировать ]

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

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

final Collection<?> c = new ArrayList<String>();
c.add(new Object()); // compile-time error
c.add(null); // allowed

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

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

Использование подстановочных знаков выше добавляет гибкости. [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;  // valid if List<Integer> were a subtype of List<Number> according to substitution rule. 
nums.add(3.14);  
final Integer x = ints.get(1); // now 3.14 is assigned to an Integer variable!

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

final List<? extends Number> nums = ints;  // OK
nums.add(3.14); // compile-time error
nums.add(null); // allowed

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

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

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

[ редактировать ]

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

public <T extends Throwable> void throwMeConditional(boolean conditional, T exception) throws T {
    if (conditional) {
        throw exception;
    }
}

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

[ редактировать ]

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

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

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

final List<Integer> li = new ArrayList<>();
final List<Float> lf = new ArrayList<>();
if (li.getClass() == lf.getClass()) { // evaluates to true
    System.out.println("Equal");
}

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

public class GenericException<T> extends 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(); //causes a compile error
}

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

Стирание типов было реализовано в 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, третье издание Джеймс Гослинг, Билл Джой, Гай Стил, Гилад Брача – Прентис Холл, PTR, 2005 г.
  6. Перейти обратно: Перейти обратно: а б с д и ж г час я дж к л м н тот Bloch 2018 , стр. 126–129, глава §5, пункт 28. Предпочитайте списки массивам.
  7. Перейти обратно: Перейти обратно: а б с д и ж г час 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. Перейти обратно: Перейти обратно: а б с д 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 г.
  • Блох, Джошуа (2018). «Эффективная Java: Руководство по языку программирования» (третье изд.). Аддисон-Уэсли. ISBN  978-0134685991 .
Arc.Ask3.Ru: конец переведенного документа.
Arc.Ask3.Ru
Номер скриншота №: fa93226d2921a43f10d347388e334fd0__1719676980
URL1:https://arc.ask3.ru/arc/aa/fa/d0/fa93226d2921a43f10d347388e334fd0.html
Заголовок, (Title) документа по адресу, URL1:
Generics in Java - Wikipedia
Данный printscreen веб страницы (снимок веб страницы, скриншот веб страницы), визуально-программная копия документа расположенного по адресу URL1 и сохраненная в файл, имеет: квалифицированную, усовершенствованную (подтверждены: метки времени, валидность сертификата), открепленную ЭЦП (приложена к данному файлу), что может быть использовано для подтверждения содержания и факта существования документа в этот момент времени. Права на данный скриншот принадлежат администрации Ask3.ru, использование в качестве доказательства только с письменного разрешения правообладателя скриншота. Администрация Ask3.ru не несет ответственности за информацию размещенную на данном скриншоте. Права на прочие зарегистрированные элементы любого права, изображенные на снимках принадлежат их владельцам. Качество перевода предоставляется как есть. Любые претензии, иски не могут быть предъявлены. Если вы не согласны с любым пунктом перечисленным выше, вы не можете использовать данный сайт и информация размещенную на нем (сайте/странице), немедленно покиньте данный сайт. В случае нарушения любого пункта перечисленного выше, штраф 55! (Пятьдесят пять факториал, Денежную единицу (имеющую самостоятельную стоимость) можете выбрать самостоятельно, выплаичвается товарами в течение 7 дней с момента нарушения.)