☕ Java

Generics und parametrische Typen in Java: Eine ausführliche Anleitung

What is it and why? (a real-life analogy)

Imagine an ordinary box. You can put anything in it: a book, a toy, an apple. But if you label the box "Books," you will only put books in it. If you label it "Toys," you will only put toys in it. And if you leave the box unlabeled, you can put anything in it, but it's easy to get confused and pull out an apple instead of a book.

Generics in Java are like these "smart labeled boxes." They allow you to create a single class or method that works with different data types, while strictly controlling which specific type is used in each case.

Why is this needed? Imagine you are writing a program for a library. You need to store a list of books. Without generics, you would write:

// Without generics — dangerous and inconvenient
ArrayList list = new ArrayList();
list.add("War and Peace");      // string
list.add(42);                 // number — also possible!
String book = (String) list.get(0); // explicit cast required
// And if you get a number and cast it to a string — the program will crash!

With generics, everything becomes safe and clear:

// With generics — safe and convenient
ArrayList<String> list = new ArrayList<>();
list.add("War and Peace");      // only strings
// list.add(42);              // compilation error — can't put a number!
String book = list.get(0);   // no cast needed — type is known
Important: Generics work at compile time. After compilation, type information is erased — this is called "type erasure." But more on that later.

Thus, generics give us three main things:

  • Type safety — the compiler won't let you put a cat in a box for books
  • Elimination of explicit casts — no need to write (String) every time
  • Code reuse — one class for all types

Theory: how it works internally

Let's look under the hood of Java and understand how generics actually work.

Parametric Polymorphism

In programming, there is the concept of polymorphism — the ability of the same code to work with different data types. Generics implement parametric polymorphism: a class or method takes a type as a parameter (like a number in a mathematical function).

Type Erasure

This is the most important thing to understand about generics in Java. Unlike C++ or C#, Java does not store generic type information during program execution. The compiler checks the types and then "erases" them, replacing them with their bounds (or with Object if there are no bounds).

// Original code with generics
public class Box<T> {
    private T content;
    
    public void set(T content) {
        this.content = content;
    }
    
    public T get() {
        return content;
    }
}

// After compilation (type erasure) — approximately like this:
public class Box {
    private Object content;  // T replaced by Object
    
    public void set(Object content) {
        this.content = content;
    }
    
    public Object get() {
        return content;
    }
}
Important: Due to type erasure, you cannot create an array of generics (new ArrayList<String>[10] — error!) and you cannot use instanceof with parameterized types.

Bounded Type Parameters

Sometimes you need to restrict which types can be used. For example, so that only numbers can be put in the box. For this, there are bounds:

  • <T extends Number> — T can only be Number or its subclasses
  • <T super Integer> — T can be Integer or its superclasses (used less often)

Wildcards

Sometimes we don't know the exact type, but we want to work with a family of types. For this, we use wildcards:

  • ? — unknown type (any)
  • ? extends Number — any type that inherits from Number
  • ? super Integer — any type that is an ancestor of Integer

Syntax in Java: all forms and variants

Let's break down all possible ways to use generics in Java. It's like a multiplication table — you need to know all the variants.

1. Generic Classes

// Class with one type parameter
public class Pair<K, V> {
    private K key;
    private V value;
    
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() { return key; }
    public V getValue() { return value; }
}

// Usage:
Pair<String, Integer> pair = new Pair<>("Age", 25);

2. Generic Methods

// Method that works with any type
public class Utils {
    public static <T> T getMiddle(T... args) {
        return args[args.length / 2];
    }
}

// Call:
String middle = Utils.<String>getMiddle("one", "two", "three");
// Or simpler (Java infers the type):
String middle2 = Utils.getMiddle("one", "two", "three");

3. Generic Interfaces

// Interface with a type parameter
public interface Repository<T> {
    void save(T item);
    T findById(int id);
    void delete(T item);
}

// Implementation for a specific type
public class UserRepository implements Repository<User> {
    @Override
    public void save(User user) { /* ... */ }
    
    @Override
    public User findById(int id) { /* ... */ }