Дженерики позволяют абстрагироваться от типа объекта. Дженерики были представлены в java 1.5.
Повестка дня этой статьи:
- Обзор дженериков
- Общие методы
- Границы
- Подстановочные знаки
Обзор дженериков
Давайте рассмотрим пример
List list = new ArrayList(); list.add("apple"); list.add("kiwi");
В приведенном выше примере мы использовали список без использования дженериков.
Если тип не указан в ‹›, типом списка по умолчанию является Object
Это эквивалентно List‹Object›
Теперь давайте посмотрим с дженериками
List<String> list = new ArrayList<>(); list.add("apple"); list.add("kiwi");
new ArrayList‹String›() и new ArrayList‹›() эквивалентны. Компилятор может узнать тип по контексту, а второй - просто более короткую версию. Это будущее доступно с Java 1.7
‹String› определяет, какой тип данных должен содержать наш список. Так зачем же нам нужны дженерики, если без них все прекрасно работает?
Мы хотим писать наш код с меньшим количеством ошибок. Используя дженерики, мы можем контролировать, какой тип данных может использоваться нашим списком, и если мы будем использовать разные типы данных, мы получим ошибку компилятора.
Давайте посмотрим еще один пример без дженериков
- Мы видим, что это позволило StringBuilder попасть в наш список. Поскольку его список Object и StringBuilder является дочерним по отношению к объекту, так что это совершенно нормально для компилятора.
- И во время выполнения, когда мы возвращаем элементы из списка, пытаясь преобразовать их в String, мы получаем наше исключение.
Теперь давайте посмотрим на пример с дженериками.
- Мы получаем ошибки компилятора, когда пытаемся добавить StringBuilder в наш список. Ошибка во время компиляции всегда лучше, чем во время выполнения.
Итак, мы использовали дженерики там, где они уже были созданы для нас. Теперь давайте создадим что-нибудь сами.
Bucket<E>
мы упоминаем в объявлении класса, что мы используем дженерики, а затем внутри нашего класса мы можем использовать его, поскольку у нас есть этот тип.- Фактический тип
<E>
будет назначен кодом клиента. - В основном методе посмотрите, как наш класс
Bucket
может принимать строку, а затем целое число.
Поясним некоторые определения:
Bucket<E>
когда мы используем подобные дженерики, мы называем их параметрами типа.
Bucket<String>
и когда мы фактически передаем тип для нашего универсального объекта, мы ссылаемся на него как на аргумент типа.
Наиболее часто используемые имена параметров типов:
E — Элемент
K — Ключ
N — Число
T — Тип
V — Значение
S, У, В и т. д. — второй, третий и четвертый типы
Еще один пример
<E, S, T>
именно так мы используем несколько параметров универсального типа.- Интерфейсы также могут использовать дженерики.
Общие методы
Это похоже на общие классы и интерфейсы. У нас могут быть методы с собственными параметрами типа, область действия которых ограничена методом, в котором они объявлены.
<E, S> boolean
Нам нужно указать параметры типа перед типом возврата метода, и тогда мы можем использовать их в качестве аргументов методаisSame(E element, S secondElement)
Границы
Что, если мы хотим использовать дженерики, но не хотим, чтобы клиентский код передавал какой-либо тип объекта. Границы помогают нам ограничить типы, которые могут быть переданы в качестве аргументов типа.
- Мы используем
extends
для указания исходящих. Каждый класс, включая сам связанный класс, который расширяет/реализует наш связанный класс, может использоваться в качестве аргумента типа для дженериков.
Почему бы просто не иметь private Car car;
в классе Garage вместо private T car;
. Здесь, собственно, та же причина — меньше дефектов. Лучше иметь ошибку компиляции, чем исключение во время выполнения.
Garage g = new Garage(); g.putInside(new BMW()); Audi audi = (Audi)g.getOut(); // runtime exception // ------ Garage<Audi> g1 = new Garage<>(); g1.putInside(new BMW()); // does not compile here
Подстановочные знаки
Мы можем использовать подстановочные знаки, чтобы обойти ограничения дженериков. ?
означает каждый тип.
Подстановочный знак с верхней границей
Допустим, мы хотим создать метод, который может принимать List<Number>
, List<Integer>
, List<Double>
, которые мы можем использовать в качестве типа аргумента нашего метода List<? extends Number>
.
Integer
иDouble
расширяютNumber
, поэтому мы можем безопасно передавать их список в наш метод.- Верхняя граница позволяет нам использовать неизвестный тип (?) или его подтип.
Подстановочный знак нижней границы
Противопоставляется верхней границе. Допустим, у нас есть эта структура
Audi -> Car -> Object ---------------------- public static void printList(List<? super Audi> list) { // code here.. }
- Метод
printList
теперь может приниматьList<Audi>, List<Car>, List<Object>
Неограниченный подстановочный знак
Допустим, мы хотим создать метод, способный распечатать список любого объекта.
public static void printList(List<Object> list) { for (Object el : list) { System.out.println(el); } }
Это правильная реализация? Нет. Например, он не будет работать с List<String>
, на самом деле он будет работать только с List<Object>
.
Теперь давайте посмотрим на правильную реализацию
public static void printList(List<?> list) { for (Object el : list) { System.out.println(el); } }
- Теперь мы можем передать List с любым типом
Краткое содержание
Дженерики позволяют абстрагироваться от типа объекта. Используя дженерики, мы избегаем исключений во время выполнения и выявляем проблемы с типами во время компиляции. Существует концепция под названием Type Erasure, которая фактически заменяет все параметры типа в универсальных типах их границами или типом объекта.
Спасибо за чтение.
Ресурсы, использованные в этой статье:
1. Документация по дженерикам от Oracle
Please take my Java Course for video lectures. This article is part of the series of articles to learn Java programming language from Tech Lead Academy: Introduction to programming OS, File, and File System Working with terminal Welcome to Java Programming Language Variables and Primitives in Java Convert String to numeric data type Input from the terminal in Java Methods with Java Java Math Operators and special operators Conditional branching in Java Switch statement in Java Ternary operator in Java Enum in Java String class and its methods in Java Loops in Java Access modifiers in Java Static keyword in Java The final keyword in Java Class and Object in Java Object-Oriented Programming in Java OOP: Encapsulation in Java OOP: Inheritance in Java OOP: Abstraction in Java OOP: Polymorphism in Java The method Overriding vs Overloading in Java Array in Java Data Structures with Java Collection framework in Java ArrayList in Java Set in Java Map in Java Date and Time in Java Exception in Java How to work with files in Java Design Patterns Generics in Java Multithreading in java Annotations in Java Reflection in Java Reflection & Annotations - The Powerful Combination Run terminal commands from Java Lambda in Java Unit Testing in Java Big O Notation for coding interviews Top Java coding interview questions for SDET