Skip to content
IRC-CodingIRC-Coding
GenericsType SafetyType ParametersWildcardsBounded TypesАлгоритмыОсновы

Generics: типобезопасность и параметры типов

Generics обеспечивают типобезопасное переиспользование кода. Type Erasure, Wildcards, Bounded Type Parameters в Java, C# и C++.

S

schutzgeist

8 min read
Generics: типобезопасность и параметры типов

Обобщённые классы: Generics, Type Safety и Type Parameters

В этой статье мы разберём обобщённые классы во всех деталях: Generics, Type Safety, Wildcards и Bounded Type Parameters с практическими примерами.

In a Nutshell

Обобщённые классы — это шаблоны типов, которые через Type Parameters позволяют создавать переиспользуемые и типобезопасные структуры данных и алгоритмы. Примеры: List<T>, Map<K,V> или C++ Templates.

Компактное описание

Обобщённые классы инкапсулируют поведение независимо от конкретного типа данных и инстанцируются через Type Arguments. Они обеспечивают типобезопасное переиспользование кода без дублирования.

Реализация в разных языках:

Java Generics

  • Type Erasure: информация о типах удаляется во время компиляции
  • Во время выполнения: только Raw Types, без обобщённых типов
  • Wildcards: ? extends Number, ? super Number
  • Bounded Types: <T extends Number>

C# Generics

  • Reified Generics: информация о типах сохраняется во время выполнения
  • Constraints: where T : class, new()
  • Covariance/Contravariance: out T, in T
  • Performance: без overhead на boxing/unboxing

C++ Templates

  • Compile-Time: для каждого типа генерируется специализированная версия
  • Zero Cost: без runtime overhead
  • Template Metaprogramming: Turing-полнота
  • SFINAE: Substitution Failure Is Not An Error

Основные концепции:

  • Инвариантность: List<String> не является List<Object>
  • Ковариантность: Producer extends (может поставлять)
  • Контравариантность: Consumer super (может потреблять)

Ключевые моменты

  • Generics: шаблоны типов для переиспользуемого кода
  • Type Safety: проверка типов во время компиляции вместо runtime ошибок
  • Type Parameters: заполнители для конкретных типов
  • Bounded Types: ограничения для Type Parameters
  • Wildcards: неизвестные типы с ограничениями
  • Type Erasure: реализация в Java без типов во время выполнения
  • PECS Rule: Producer Extends, Consumer Super
  • Важно для типобезопасного программирования

Основные компоненты

  1. Type Parameter: заполнитель <T> для конкретных типов
  2. Bounded Types: <T extends Number> с ограничениями
  3. Wildcards: <?> для неизвестных типов
  4. Generic Methods: <T> T max(T a, T b)
  5. Generic Classes: class Box<T>
  6. Type Safety: проверка типов на этапе компиляции
  7. Covariance: отношение Producer extends
  8. Contravariance: отношение Consumer super

Практические примеры

1. Простой обобщённый класс на Java

// Обобщённый класс Box
public class Box<T> {
    private T inhalt;
    
    public Box() {
        this.inhalt = null;
    }
    
    public Box(T inhalt) {
        this.inhalt = inhalt;
    }
    
    public void setInhalt(T inhalt) {
        this.inhalt = inhalt;
    }
    
    public T getInhalt() {
        return inhalt;
    }
    
    public boolean istLeer() {
        return inhalt == null;
    }
    
    @Override
    public String toString() {
        return "Box[" + (inhalt != null ? inhalt.toString() : "leer") + "]";
    }
}

// Использование обобщённого класса
public class BoxDemo {
    public static void main(String[] args) {
        // Box для String
        Box<String> stringBox = new Box<>("Hallo Welt");
        System.out.println(stringBox); // Box[Hallo Welt]
        
        // Box для Integer
        Box<Integer> integerBox = new Box<>(42);
        System.out.println(integerBox); // Box[42]
        
        // Box для собственного объекта
        class Person {
            String name;
            Person(String name) { this.name = name; }
            @Override public String toString() { return name; }
        }
        
        Box<Person> personBox = new Box<>(new Person("Max"));
        System.out.println(personBox); // Box[Max]
        
        // Type Safety во время компиляции
        // stringBox.setInhalt(123); // Ошибка: невозможно присвоить Integer к String
    }
}

2. Обобщённые методы

public class GenericMethods {
    
    // Обобщённый метод для обмена элементов
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    // Обобщённый метод с Bounded Type Parameter
    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
    
    // Обобщённый метод с Wildcard
    public static void printList(List<?> list) {
        for (Object element : list) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
    
    // PECS: Producer Extends
    public static double sum(List<? extends Number> list) {
        double sum = 0.0;
        for (Number n : list) {
            sum += n.doubleValue();
        }
        return sum;
    }
    
    // PECS: Consumer Super
    public static void addNumbers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }
    
    public static void main(String[] args) {
        // Метод swap
        String[] names = {"Alice", "Bob", "Charlie"};
        swap(names, 0, 2);
        System.out.println(Arrays.toString(names)); // [Charlie, Bob, Alice]
        
        // Метод max
        System.out.println(max(5, 10)); // 10
        System.out.println(max("Apple", "Banana")); // Banana
        
        // Wildcard
        List<String> strings = Arrays.asList("A", "B", "C");
        List<Integer> numbers = Arrays.asList(1, 2, 3);
        
        printList(strings); // A B C
        printList(numbers); // 1 2 3
        
        // Примеры PECS
        List<Integer> ints = Arrays.asList(1, 2, 3);
        System.out.println(sum(ints)); // 6.0
        
        List<Object> objects = new ArrayList<>();
        addNumbers(objects);
        System.out.println(objects); // [1, 2, 3]
    }
}

3. Обобщённый класс с Bounded Types

// Обобщённый класс с несколькими Bounded Type Parameters
public class Paar<T extends Comparable<T>, U> {
    private T erster;
    private U zweiter;
    
    public Paar(T erster, U zweiter) {
        this.erster = erster;
        this.zweiter = zweiter;
    }
    
    public T getErster() {
        return erster;
    }
    
    public U getZweiter() {
        return zweiter;
    }
    
    public void setErster(T erster) {
        this.erster = erster;
    }
    
    public void setZweiter(U zweiter) {
        this.zweiter = zweiter;
    }
    
    // Метод, использующий Bounded Type
    public int vergleicheErsten(T anderer) {
        return this.erster.compareTo(anderer);
    }
    
    @Override
    public String toString() {
        return "Paar[" + erster + ", " + zweiter + "]";
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        
        Paar<T, U> paar = (Paar<T, U>) obj;
        return erster.equals(paar.erster) && zweiter.equals(paar.zweiter);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(erster, zweiter);
    }
}

// Использование
public class PaarDemo {
    public static void main(String[] args) {
        // Допустимо: String implements Comparable
        Paar<String, Integer> nameAlter = new Paar<>("Alice", 25);
        System.out.println(nameAlter); // Paar[Alice, 25]
        
        // Ошибка: StringBuilder не implements Comparable
        // Paar<StringBuilder, Integer> invalid = new Paar<>(new StringBuilder(), 25);
        
        // Использование сравнения
        Paar<String, Integer> alice = new Paar<>("Alice", 25);
        Paar<String, Integer> bob = new Paar<>("Bob", 30);
        
        int vergleich = alice.vergleicheErsten(bob.getErster());
        System.out.println("Vergleich: " + vergleich); // отрицательное значение (Alice < Bob)
    }
}

4. C# Generics с ограничениями

using System;

// Обобщённый класс с ограничениями
public class Repository<T> where T : class, new()
{
    private List<T> items = new List<T>();
    
    public void Add(T item)
    {
        items.Add(item);
    }
    
    public T GetById(int id)
    {
        return items.FirstOrDefault(item => GetId(item) == id);
    }
    
    public IEnumerable<T> GetAll()
    {
        return items;
    }
    
    // Метод, который использует ограничение new()
    public T CreateNew()
    {
        return new T(); // Возможно только благодаря ограничению new()
    }
    
    private int GetId(T item)
    {
        // Предположение: T имеет свойство Id
        var property = typeof(T).GetProperty("Id");
        if (property != null && property.PropertyType == typeof(int))
        {
            return (int)property.GetValue(item);
        }
        return 0;
    }
}

// Класс сущности
public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    public Person() { }
    
    public Person(int id, string name)
    {
        Id = id;
        Name = name;
    }
}

// Использование
public class RepositoryDemo
{
    public static void Main(string[] args)
    {
        var personRepo = new Repository<Person>();
        
        // Создание новой персоны (использует ограничение new())
        var person1 = personRepo.CreateNew();
        person1.Id = 1;
        person1.Name = "Alice";
        
        var person2 = new Person(2, "Bob");
        
        personRepo.Add(person1);
        personRepo.Add(person2);
        
        // Вывод всех персон
        foreach (var person in personRepo.GetAll())
        {
            Console.WriteLine($"ID: {person.Id}, Name: {person.Name}");
        }
        
        // Поиск персоны по ID
        var foundPerson = personRepo.GetById(1);
        Console.WriteLine($"Найдено: {foundPerson?.Name}");
    }
}

5. C++ Templates

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

// Обобщённый класс (Template)
template<typename T>
class Stack {
private:
    std::vector<T> elements;
    
public:
    void push(const T& element) {
        elements.push_back(element);
    }
    
    T pop() {
        if (elements.empty()) {
            throw std::runtime_error("Stack ist leer");
        }
        
        T element = elements.back();
        elements.pop_back();
        return element;
    }
    
    bool empty() const {
        return elements.empty();
    }
    
    size_t size() const {
        return elements.size();
    }
    
    // Метод-шаблон внутри класса
    void print() const {
        std::cout << "Stack [";
        for (size_t i = 0; i < elements.size(); ++i) {
            std::cout << elements[i];
            if (i < elements.size() - 1) {
                std::cout << ", ";
            }
        }
        std::cout << "]" << std::endl;
    }
};

// Функция-шаблон
template<typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

// Шаблон с ограниченным типовым параметром (Concept в C++20)
template<typename T>
requires std::is_arithmetic_v<T>
T calculateAverage(const std::vector<T>& numbers) {
    if (numbers.empty()) {
        return T{};
    }
    
    T sum = T{};
    for (const T& num : numbers) {
        sum += num;
    }
    
    return sum / static_cast<T>(numbers.size());
}

int main() {
    // Stack для разных типов
    Stack<int> intStack;
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);
    
    std::cout << "Int-Stack: ";
    intStack.print();
    
    Stack<std::string> stringStack;
    stringStack.push("Hallo");
    stringStack.push("Welt");
    
    std::cout << "String-Stack: ";
    stringStack.print();
    
    // Функция-шаблон
    std::cout << "Max(5, 10): " << max(5, 10) << std::endl;
    std::cout << "Max(3.14, 2.71): " << max(3.14, 2.71) << std::endl;
    
    // Ограниченный шаблон
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::cout << "Durchschnitt: " << calculateAverage(numbers) << std::endl;
    
    return 0;
}

Правило PECS: Producer Extends, Consumer Super

Producer (поставщик данных)

// Producer: читает из списка, возвращает данные
public static void processList(List<? extends Number> list) {
    for (Number num : list) {
        System.out.println(num.doubleValue()); // Можно вызывать методы Number
    }
    // list.add(42); // Ошибка: нельзя писать в Producer
}

Consumer (потребитель данных)

// Consumer: пишет в список, потребляет данные
public static void fillList(List<? super Integer> list) {
    list.add(1);    // Integer можно добавить
    list.add(2);    // Number можно добавить
    list.add(3);    // Object можно добавить
}

Type Erasure в Java

Что происходит при компиляции?

// Исходный код
public class Box<T> {
    private T content;
    
    public void setContent(T content) {
        this.content = content;
    }
    
    public T getContent() {
        return content;
    }
}

// После Type Erasure (что видит JVM)
public class Box {
    private Object content;
    
    public void setContent(Object content) {
        this.content = content;
    }
    
    public Object getContent() {
        return content;
    }
}

Приведение типов во время выполнения

Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");

// Неявное приведение при возврате
String content = stringBox.getContent(); // Приведение Object к String

// Проверка типов при компиляции
// stringBox.setContent(123); // Ошибка компиляции

Преимущества и недостатки

Преимущества обобщённых типов

  • Type Safety: Ошибки на этапе компиляции вместо runtime
  • Переиспользование: Один код для разных типов
  • Читаемость: Явная информация о типах
  • Поддерживаемость: Меньше дублирования кода
  • Производительность: Без boxing/unboxing для типов-значений (C#)

Недостатки

  • Сложность: Нужно разбираться с wildcards и bounds
  • Type Erasure: Java теряет информацию о типах во время выполнения
  • Overhead компиляции: Дольше компилируются C++ templates
  • Binary compatibility: Изменения могут сломать существующий код

Частые вопросы на экзаменах

  1. Что такое Type Erasure в Java? Информация о типах удаляется при компиляции, во время выполнения остаются только Raw Types.

  2. Объясните правило PECS! Producer Extends (только чтение), Consumer Super (только запись).

  3. Чем отличается List<?> от List<Object>? List<?> это неизвестный тип, List<Object> это конкретно Object.

  4. Зачем нужны Bounded Type Parameters? Чтобы добавить ограничения и использовать методы типа.

Основные источники

  1. https://docs.oracle.com/javase/tutorial/java/generics/
  2. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/
  3. https://en.cppreference.com/w/cpp/language/templates
Назад к блогу
Share:

Nächster Artikel in Разработка программного обеспечения

Weiterlesen
ООП Абстракция: интерфейсы и Design by Contract

Похожие статьи