Generics в Java: пример с Promotion и Customer

Generics — это концепция программирования, позволяющая указать тип данных, который будет храниться в коллекции или использоваться в классе/методе. Обобщённый (generic) тип обычно представляет собой ссылочный тип (не примитив). В практическом примере ниже мы используем Customer как базовый обобщённый тип и создаём типо-безопасный класс Promotion
Краткое объяснение ключевых терминов
- Generics: механизм параметризации типа в Java.
- Параметр типа: буква или имя в угловых скобках, например T или E.
- Ограничение (bounded type): запись T extends Customer ограничивает допустимые типы.
Создание обобщённого типа (Customer)
В примере Customer реализован как абстрактный класс — это означает, что напрямую объекты Customer создать нельзя; нужно использовать подклассы. Абстрактный базовый тип служит «контрактом» для конкретных типов клиентов из разных городов.
public abstract class Customer {
private String name;
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
}Обратите внимание: ключевое слово abstract запрещает создание экземпляра Customer напрямую.
Каждое физическое местоположение магазина имеет собственный подкласс Customer. Это даёт возможность различать клиентов по «жёстким» типам:
public class City1Customer extends Customer{
public City1Customer(String name) {
super(name);
}
}(Аналогично можно создать City2Customer и City3Customer.)
Создание обобщённого класса Promotion
Чтобы использовать обобщённый тип, нужен обобщённый класс или метод, принимающий этот тип как параметр. В объявлении класса вместо конкретного типа ставится параметр типа в угловых скобках:
public class Promotion {} Если нужно ограничить допустимые типы только подтипами Customer, используется ограничение:
public class Promotion {} Это значит: Promotion может параметризоваться только типами, унаследованными от Customer.
Полная реализация класса Promotion, используемая в примере:
import java.util.ArrayList;
public class Promotion {
private String promoName;
private ArrayList winners = new ArrayList<>();
public Promotion(String promoName) {
this.promoName = promoName;
}
public String getPromoName() {
return promoName;
}
public void addCustomer(T customer) {
if (winners.contains(customer)) {
System.out.println( customer.getName() + " is already a winner of this prize.");
} else {
winners.add(customer);
System.out.println( customer.getName() + " is a winner in the " + this.promoName);
}
}
public int numWinners() {
return this.winners.size();
}
} Класс Promotion хранит имя акции promoName и список победителей winners. Метод addCustomer добавляет клиента, предотвращая дубли, а numWinners возвращает количество победителей.
Создание коллекций с использованием обобщённого класса
Сначала создаём объекты клиентов:
public class Main {
public static void main(String[] args) {
City1Customer john = new City1Customer("John Brown");
City1Customer kelly = new City1Customer("Kelly James");
City2Customer jane = new City2Customer("Jane Doe");
City3Customer jess = new City3Customer("Jess Smith");
}
}Далее создаём объекты Promotion, параметризованные соответствующими типами клиентов:
Promotion city1promo = new Promotion("City1 Promo");
Promotion city2promo = new Promotion("City2 Promo");
Promotion city3promo = new Promotion("City3 Promo"); Затем добавляем клиентов в соответствующие промо-списки:
city1promo.addCustomer(john);
city1promo.addCustomer(kelly);В консоли это даст вывод победителей для первого города:
Если попытаться добавить клиента из другого города в список не его города, IDE укажет на ошибку компиляции:
Это защищает данные: Jane и Jess не являются клиентами первого магазина, и их нельзя добавить в Promotion
Правильный вызов для второго города:
city2promo.addCustomer(jane);Результат для второго города:
Преимущества использования обобщённых типов
- Проверка типов во время компиляции: исключает ошибки несовместимых типов до запуска программы.
- Переиспользуемость кода: один класс Promotion заменяет три почти идентичных подкласса.
- Масштабируемость: при расширении компании на новые города достаточно добавить новый подкласс Customer и параметризовать Promotion.
- Ясность намерений API: сигнатура Promotion
явно показывает, какие типы допустимы.
Когда generics не подходят или вызывают сложности
- Нужны операции с примитивами: Java-дженерики работают только с ссылочными типами, не с примитивами (int, boolean). В таких случаях используют упаковочные типы (Integer, Boolean) или специализированные коллекции (например, IntStream, Trove, fastutil).
- Нужна сериализация с сохранением конкретного типа времени выполнения: из-за стирания типов (type erasure) информация о параметре типа недоступна в рантайме без дополнительных приёмов (Class
в конструкторе или TypeToken). - Сложные правила ковариантности/контравариантности: для чтения/записи нужны wildcard-типовые параметры (? extends T, ? super T), что усложняет API.
Пример: если нужен список, который может принимать и возвращать разные подтипы Customer в гибкой форме, придётся использовать подходы с wildcard или дополнительными интерфейсами.
Альтернативные подходы
- Композиция вместо наследования: хранить в поле Customer, а не наследовать от него, если поведение сильно различается.
- Runtime-проверки: использовать Object и проверять тип во время выполнения (устаревший и опасный подход).
- Шаблоны проектирования (стратегия, фабрика): обеспечить различное поведение промо через паттерны, а не через разные типы.
Практическая методология внедрения generics (мини-SOP)
- Определите доменные сущности, которые могут выступать параметризованными типами (например, Customer).
- Выделите повторяющуюся логику (Promotion) и сделайте её параметризованной типом.
- Ограничьте параметр типа (T extends Customer), если нужно контроль допустимых типов.
- Добавьте unit-тесты для типичных сценариев: добавление, дубликаты, границы.
- При необходимости используйте Class
для сохранения информации о типе в рантайме.
Чек‑лист по ролям
- Разработчик:
- Использовать bounded generics для доменных ограничений.
- Не забывать про equals/hashCode для объектов, которые хранятся в списках/множествах.
- Архитектор:
- Оценить влияние type erasure на сериализацию и отражение.
- Решить, где нужна ковариантность через wildcard.
- Тестировщик:
- Проверить, что добавление неподходящего типа компилируется с ошибкой.
- Написать тесты на дедупликацию и границы списка.
Тестовые случаи и критерии приёмки
Критерии приёмки:
- Promotion
принимает только подклассы Customer. - При добавлении одного и того же объекта дважды не происходит дублирования.
- numWinners() возвращает корректное число победителей.
- Promotion
Тестовые сценарии:
- Добавить двух разных клиентов одного города — ожидается size() == 2.
- Добавить один и тот же объект дважды — ожидается size() == 1 и сообщение о том, что клиент уже выиграл.
- Попытка компиляции при добавлении клиента другого города — тест компиляции/статического анализа.
Ментальные модели и эвристики
- Модель «контракта»: обобщённый тип — это обещание: “Promotion будет работать только с типами, которые соответствуют Customer”.
- Эвристика «параметризация вместо наследования»: если у вас повторяется одна и та же структура с безопасной вариацией по типам данных — используйте generics.
Короткий словарь (1 строка)
- Generics: параметризация типов в Java для обеспечения типовой безопасности и повторного использования кода.
Риски и смягчения
- Риск: потеря информации о типе в рантайме (type erasure). Смягчение: передавать Class
при создании или использовать сериализацию с метаданными. - Риск: неправильное использование wildcard (? extends / ? super) ведёт к сложному API. Смягчение: документировать намерения API и давать примеры использования.
Краткое резюме
Generics в Java — мощный инструмент для написания типобезопасного, повторно используемого и масштабируемого кода. Пример с Promotion
Важно: выбор между generics, композицией или runtime-проверками зависит от требований проекта: безопасности типов, производительности и потребностей сериализации.