Введение
В любом нетривиальном программном проекте ошибки — это просто факт жизни. Тщательное планирование, программирование и тестирование могут помочь уменьшить их распространенность, но так или иначе, где-то они всегда найдут способ проникнуть в ваш код. Это становится особенно очевидным, когда вводятся новые функции, а ваша кодовая база увеличивается в размерах и усложняется.
К счастью, некоторые ошибки обнаружить легче, чем другие. Ошибки времени компиляции, например, можно обнаружить на ранней стадии; вы можете использовать сообщения об ошибках компилятора, чтобы найти проблему и исправить ее. Однако ошибки во время выполнения могут быть гораздо более неприятными; они не всегда появляются сразу, а если и появляются, то в программе, которая может быть удалена от фактической причины проблемы.
Обобщения, представленные в J2SE 5.0, предоставили долгожданное усовершенствование системы типов, которое позволяет типу или методу работать с объектами нескольких типов, обеспечивая при этом безопасность типов во время компиляции. Он добавляет безопасность типов во время компиляции в Collections Framework и устраняет нудную работу по приведению типов.
Преимущества дженериков
- Безопасность типов: предположим, вы хотите сохранить название некоторых книг в ArrayList и по ошибке добавили целочисленное значение вместо строки. Компилятор разрешает это, но проблема возникает, когда вы хотите его получить. Поэтому компилятор выдает ошибку времени выполнения. При использовании универсального компилятор отображает ошибки во время компиляции, а не во время выполнения. Это экономит время программиста, потому что трудно найти ошибку во время выполнения. Всегда лучше находить ошибки во время компиляции, чем во время выполнения.
- Приведение типов не требуется: приведение типов в каждой операции восстановления — большая головная боль. Если у нас есть список, содержащий имена животных в зоопарке, нам не нужно приводить данные каждый раз, когда мы выполняем операцию восстановления, если мы уже знаем, что наш список содержит только строковые данные, нам не нужно выполнять приведение типов каждый раз.
- Повторное использование кода: мы можем писать универсальные методы, классы и интерфейсы. Чтобы мы могли повторно использовать код. Как видите, платформа Collection предоставляет множество универсальных классов. Вы можете использовать их в соответствии с вашим типом данных.
Соглашения об именах параметров типов
По соглашению имена параметров типа состоят из одной буквы и заглавных букв. Это резко контрастирует с соглашениями об именах переменных, с которыми вы уже знакомы, и на то есть веская причина: без этого соглашения было бы трудно отличить переменную типа от имени обычного класса или интерфейса.
Наиболее часто используемые имена параметров типа:
• E — элемент (широко используется Java Collections Framework).
• К — ключ
• N — номер
• Т — Тип
• V — значение
• С, У, В и др. — 2-й, 3-й, 4-й типы
Вы увидите, что эти имена используются во всем API Java SE.
Быстрый пример
Как вы можете видеть в классе Box ниже, его методы принимают или возвращают объект, вы можете передавать все, что хотите, если это не один из примитивных типов. Невозможно проверить во время компиляции, как используется класс. Один фрагмент кода может поместить целое число в поле и ожидать, что из него будут получены целые числа, в то время как другой фрагмент кода может по ошибке передать строку, что приведет к ошибке времени выполнения.
Теперь давайте превратим класс Box в универсальный класс.
Как вы можете видеть, все экземпляры Object заменяются на T. Переменная типа может быть любым указанным вами непримитивным типом: любым типом класса, любым типом интерфейса, любым типом массива или даже другим типом переменной.
Чтобы сослаться на универсальный класс Box из своего кода, вы должны выполнить вызов универсального типа, который заменяет T некоторым конкретным значением, таким как Integer:
Вы можете думать о вызове универсального типа как о вызове обычного метода, но вместо передачи аргумента методу вы передаете аргумент типа — в данном случае Integer — самому классу Box.
Как и любое другое объявление переменной, этот код на самом деле не создает новый объект Box. Он просто объявляет, что integerBox будет содержать ссылку на «Integer Box», именно так Box читается.
Вызов универсального типа обычно называют параметризованным типом. Чтобы создать экземпляр этого класса, используйте ключевое слово new как обычно, но поместите ‹Integer› между именем класса и скобками:
В Java SE 7 и более поздних версиях вы можете заменить аргументы типа, необходимые для вызова конструктора универсального класса, пустым набором аргументов типа (‹›), если компилятор может определить или вывести аргументы типа из контекста. Эта пара угловых скобок ‹› неофициально называется Dромб. Например, вы можете создать экземпляр Box‹Integer› с помощью следующего оператора:
Общие методы
Универсальные методы — это методы, которые вводят собственные параметры типа. Это похоже на объявление универсального типа, но область действия параметра типа ограничена методом, в котором он объявлен. Допускаются статические и нестатические универсальные методы, а также конструкторы универсальных классов.
Синтаксис универсального метода включает в себя список параметров типа, заключенных в угловые скобки, которые появляются перед возвращаемым типом метода. Для статических универсальных методов раздел параметров типа должен стоять перед возвращаемым типом метода.
Класс Util включает универсальный метод compare, который сравнивает два объекта Pair:
Параметры ограниченного типа
Могут быть случаи, когда вы хотите ограничить типы, которые можно использовать в качестве аргументов типа в параметризованном типе. Например, метод, работающий с числами, может принимать только экземпляры класса Number или его подклассов. Вот для чего нужны параметры ограниченного типа.
Чтобы объявить параметр ограниченного типа, перечислите имя параметра типа, за которым следует ключевое слово extends, за которым следует его верхняя граница, в данном примере — Number. Обратите внимание, что в этом контексте extends используется в общем смысле для обозначения «расширяет» (как в классах) или «реализует» (как в интерфейсах).
Теперь давайте создадим новый класс Box и запустим наш метод проверки.
При изменении нашего универсального метода для включения этого параметра ограниченного типа компиляция завершится ошибкой, поскольку наш вызов inspect включает строку и принимает только расширенный тип класса Number.
Несколько границ
Переменная типа с несколькими границами является подтипом всех типов, перечисленных в границе. Если одна из границ является классом, она должна быть указана первой.
Общие методы и параметры ограниченного типа
Параметры ограниченного типа являются фундаментальными для реализации универсальных алгоритмов. Рассмотрим следующий метод, который подсчитывает количество элементов в массиве T[], превышающих указанный элемент elem.
Реализация метода проста, но он не компилируется, потому что оператор «больше чем» (›) применяется только к примитивным типам, таким как short, int, double, long, float, byte и char. Вы не можете использовать оператор › для сравнения объектов.
Чтобы решить эту проблему, используйте параметр ограниченного типа в интерфейсе Comparable:
Запуск примера.
В приведенном выше коде мы объявляем массив целых чисел и переменную greatThan4 для хранения количества чисел больше четырех, присутствующих в массиве. Затем мы вызываем статический метод countGreaterThan, чтобы вернуть количество чисел больше 4 в массиве целых чисел, а затем выводим значение переменной GreaterThan4 в консоль.
Подстановочные знаки
В универсальном коде знак вопроса (?), называемый подстановочным знаком, представляет неизвестный тип. Подстановочный знак можно использовать в нескольких ситуациях: как тип параметра, поля или локальной переменной; иногда как тип возвращаемого значения (хотя лучше быть более конкретным в практике программирования). Подстановочный знак никогда не используется в качестве аргумента типа для вызова универсального метода, создания экземпляра универсального класса или супертипа.
Подстановочные знаки с верхней границей
Вы можете использовать подстановочный знак с верхней границей, чтобы ослабить ограничения на переменную. Например, предположим, что вы хотите написать метод, который работает со списками List‹Integer›, List‹Double› и List‹Number›; вы можете добиться этого, используя подстановочный знак с верхней границей.
Чтобы объявить подстановочный знак с верхней границей, используйте символ ('?'), за которым следует ключевое слово extends, за которым следует его верхняя граница. Обратите внимание, что в этом контексте extends используется в общем смысле для обозначения «расширяет» (как в классах) или «реализует» (как в интерфейсах).
Чтобы написать метод, работающий со списками чисел и подтипами чисел, такими как Integer, Double и Float, вы должны указать List‹? расширяет номер›. Термин Список‹Номер› является более ограничительным, чем Список‹? расширяет Number›, поскольку первый соответствует только списку типа Number, а второй соответствует списку типа Number или любому из его подклассов.
Рассмотрим следующий метод для вывода списка объектов типа Number или любого из его подклассов.
Теперь давайте создадим экземпляр класса Sample и запустим метод listNumbers.
Неограниченные подстановочные знаки
Тип Unbounded Wildcard задается с помощью символа (?), например, List‹?›. Это называется списком неизвестного типа. Есть два сценария, в которых неограниченный подстановочный знак является полезным подходом:
- Если вы пишете метод, который может быть реализован с использованием функций, предоставляемых в классе Object.
- Когда код использует методы универсального класса, которые не зависят от параметра типа. Например, List.size или List.clear. На самом деле Class‹?› используется так часто, потому что большинство методов в Class‹T› не зависят от T.
Рассмотрим следующий метод printList:
Целью printList является печать списка любого типа, но он не может достичь этой цели, он просто печатает список экземпляров Object; он не может печатать List‹Integer›, List‹String›, List‹Double› и т. д., поскольку они не являются подтипами List‹Object›. Чтобы написать общий метод printList, используйте List‹?›:
Что касается любого конкретного типа A, List‹A› является подтипом List‹?›, вы можете использовать printList для печати списка любого типа.
Подстановочные знаки с нижней границей
Раздел «Подстановочный знак с верхней границей» показывает, что подстановочный знак с верхней границей ограничивает неизвестный тип определенным типом или подтипом этого типа и представляется с помощью ключевого слова extends. Точно так же нижняя граница подстановочного знака ограничивает неизвестный тип определенным типом или супертипом этого типа.
Подстановочный знак с нижней границей определяется с помощью символа ('?'), за которым следует ключевое слово super, за которым следует его нижняя граница: ‹? супер А›.
Допустим, вы хотите написать метод, который помещает объекты Integer в список. Чтобы максимизировать гибкость, вы хотите, чтобы метод работал со списком‹Целое›, списком‹Число› и списком‹Объектом› — со всем, что может содержать целочисленные значения.
Термин List‹Integer› является более ограничительным, чем List‹? super Integer›, поскольку первое соответствует только списку типа Integer, а второе соответствует списку любого типа, который является супертипом Integer.
Выполнение addNumbers со списком Integer.
Выполнение addNumbers со списком Number.
Выполнение addNumbers со списком Object.
Заключительные соображения
В этой статье описаны основные моменты дженериков в java, но есть и другие очень важные моменты, на которые стоит обратить внимание:
- Введите стирание
- Ограничения на дженерики
- Обобщения, наследование и подтипы
- Подстановочные знаки и подтипы
- Захват подстановочных знаков и вспомогательные методы
Вы можете проверить пункты, упомянутые выше, в этой документации Oracle: https://docs.oracle.com/javase/tutorial/java/generics/index.html
Использованная литература:
https://docs.oracle.com/javase/tutorial/extra/generics/index.html