Дженерики позволяют абстрагироваться от типа объекта. Дженерики были представлены в 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