Introduction to Java Generics
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:
- No type safety — any object can be added.
- Runtime errors —
ClassCastExceptiondiscovered only when code runs. - 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
- Generics provide compile-time type safety — no more
ClassCastExceptionsurprises. - Type erasure means generic info is removed at runtime — the JVM sees raw types.
- The diamond operator
<>avoids redundant type declarations. - Never use raw types — always specify
<T>or use wildcards<?>. - Generics work with reference types only — use
Integer, notint. - 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.