Introduction to Java Generics

3 minute read

Generics let you write type-safe, reusable code. Instead of casting Object everywhere and hoping for the best, generics catch type errors at compile time.


The Problem: Life Before Generics

// Java 1.4 — no generics
List names = new ArrayList();
names.add("Alice");
names.add(42);          // no compile error — anything goes!

String first = (String) names.get(0);  // explicit cast required
String second = (String) names.get(1); // ClassCastException at runtime!

Problems:

  1. No type safety — any object can be added.
  2. Runtime errors — ClassCastException discovered only when code runs.
  3. Verbose — every get() needs a cast.

The Solution: Generics (Java 5+)

List<String> names = new ArrayList<String>();
names.add("Alice");
// names.add(42);       // ❌ Compile error! Type safety enforced.

String first = names.get(0);  // no cast needed

The <String> tells the compiler: “This list holds only Strings.”


Diamond Operator <> (Java 7+)

Java can infer the type from the left side:

// Before Java 7
List<String> names = new ArrayList<String>();

// Java 7+ — diamond operator
List<String> names = new ArrayList<>();

// Java 10+ — var with diamond
var names = new ArrayList<String>();

Generic Classes

You can define your own generic classes:

public class Box<T> {
    private T content;

    public void put(T item) {
        this.content = item;
    }

    public T get() {
        return content;
    }
}

Box<String> stringBox = new Box<>();
stringBox.put("Hello");
String value = stringBox.get();  // no cast

Box<Integer> intBox = new Box<>();
intBox.put(42);
int number = intBox.get();       // auto-unboxing

Generic Methods

Methods can have their own type parameters, independent of the class:

public static <T> T getFirst(List<T> list) {
    if (list.isEmpty()) throw new NoSuchElementException();
    return list.get(0);
}

String first = getFirst(List.of("A", "B", "C"));  // T inferred as String
Integer num = getFirst(List.of(1, 2, 3));          // T inferred as Integer

The <T> before the return type declares the method’s type parameter.


Type Erasure

Generics are a compile-time feature. At runtime, all generic type information is erased:

// What you write:
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();

// What the JVM sees (after erasure):
List strings = new ArrayList();
List numbers = new ArrayList();

// They're the same class at runtime!
strings.getClass() == numbers.getClass();  // true

Consequences of Type Erasure

// ❌ Cannot do:
if (obj instanceof List<String>) { }   // compile error
T item = new T();                        // compile error
T[] array = new T[10];                   // compile error

// ✅ Can do:
if (obj instanceof List<?>) { }         // unbounded wildcard OK

Raw Types — Don’t Use Them

A raw type is a generic class used without type parameters:

List raw = new ArrayList();     // raw type — no <T>
raw.add("String");
raw.add(42);                    // no compile error
raw.add(new Object());          // anything goes!

String s = (String) raw.get(1); // ClassCastException at runtime!

Raw types exist only for backward compatibility with pre-Java 5 code. Never use them in new code.

// ❌ Raw type
List list = new ArrayList();

// ✅ Parameterised type
List<String> list = new ArrayList<>();

// ✅ Wildcard when type is unknown
List<?> list = getUnknownList();

Type Parameter Naming Conventions

Letter Meaning Example
T Type Box<T>, List<T>
E Element Collection<E>
K Key Map<K, V>
V Value Map<K, V>
N Number Calculator<N extends Number>
S, U Second, third types Pair<S, U>

Multiple Type Parameters

public class Pair<K, V> {
    private final K key;
    private final V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey()   { return key; }
    public V getValue() { return value; }
}

Pair<String, Integer> entry = new Pair<>("age", 30);
String key = entry.getKey();      // "age"
Integer value = entry.getValue(); // 30

Generics and Primitives

Generics don’t work with primitives — only with reference types:

// ❌ Compile error
List<int> numbers = new ArrayList<>();

// ✅ Use wrapper classes
List<Integer> numbers = new ArrayList<>();
numbers.add(42);        // autoboxing: int → Integer
int value = numbers.get(0); // auto-unboxing: Integer → int

Key Takeaways

  1. Generics provide compile-time type safety — no more ClassCastException surprises.
  2. Type erasure means generic info is removed at runtime — the JVM sees raw types.
  3. The diamond operator <> avoids redundant type declarations.
  4. Never use raw types — always specify <T> or use wildcards <?>.
  5. Generics work with reference types only — use Integer, not int.
  6. Follow naming conventions: T, E, K, V.

What’s Next?

Next: Generic Classes and Methods — writing reusable generic types, static generic methods, and multiple type parameters.

Updated: