Generics
Write type-safe, reusable classes and methods using generics to eliminate casting and catch errors at compile time.
Overview
Key Points
- •Generic class: class Box<T> { T value; }
- •T is a convention for a single type; E for element, K for key, V for value
- •The compiler erases type parameters at runtime (type erasure); generics are a compile-time feature
- •Generic methods: <T> T identity(T t) can be called with any type
- •You cannot use primitives as type parameters: use Integer, not int
- •You cannot create arrays of generic types: new T[] is not allowed
- •Generic types allow code reuse without sacrificing type safety
- •Bounded type parameters restrict which types are allowed (covered in J049)
Examples
Generic class
public class Main {
// Generic container that holds any type
static class Box<T> {
private T value;
Box(T value) { this.value = value; }
T getValue() { return value; }
void setValue(T value) { this.value = value; }
@Override
public String toString() { return "Box[" + value + "]"; }
}
public static void main(String[] args) {
Box<String> stringBox = new Box<>("Hello");
Box<Integer> intBox = new Box<>(42);
Box<Double> doubleBox = new Box<>(3.14);
System.out.println(stringBox.getValue()); // Hello
System.out.println(intBox.getValue()); // 42
// Compile error prevented:
// stringBox.setValue(123); // would not compile
}
}The type parameter T is replaced at compile time. Box<String> and Box<Integer> are compile-time distinct; mistakes like setting an int into a String box are caught immediately.
Generic class with two type parameters
public class Main {
static class Pair<A, B> {
private final A first;
private final B second;
Pair(A first, B second) {
this.first = first;
this.second = second;
}
A getFirst() { return first; }
B getSecond() { return second; }
@Override
public String toString() {
return "(" + first + ", " + second + ")";
}
}
public static void main(String[] args) {
Pair<String, Integer> entry = new Pair<>("Alice", 92);
Pair<Double, Boolean> result = new Pair<>(3.14, true);
System.out.println(entry); // (Alice, 92)
System.out.println(result); // (3.14, true)
String name = entry.getFirst(); // no cast needed
int score = entry.getSecond();
System.out.println(name + " scored " + score);
}
}Multiple type parameters are separated by commas. Map.Entry<K,V> in the JDK is a real-world example of this pattern.
Generic method
import java.util.List;
import java.util.ArrayList;
public class Main {
// Generic method: works with any type T
static <T> List<T> repeat(T item, int times) {
List<T> list = new ArrayList<>();
for (int i = 0; i < times; i++) list.add(item);
return list;
}
static <T> void swap(T[] arr, int i, int j) {
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
System.out.println(repeat("Java", 3)); // [Java, Java, Java]
System.out.println(repeat(7, 4)); // [7, 7, 7, 7]
String[] words = {"hello", "world", "java"};
swap(words, 0, 2);
System.out.println(java.util.Arrays.toString(words)); // [java, world, hello]
}
}The compiler infers the type parameter from the argument. You can also specify it explicitly: Main.<String>repeat("x", 3).
Common Mistakes
- Using raw types: Box box = new Box(42) loses all type safety. Always supply the type parameter
- Trying to use primitives: Box<int> does not compile. Use Box<Integer> and rely on autoboxing
- Trying to instantiate the type parameter: new T() is not allowed because type information is erased at runtime
Note
Generics only exist at compile time. At runtime, Box<String> and Box<Integer> are both just Box. This is called type erasure and is why you cannot do instanceof checks like value instanceof T.
Exercises
Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.
Completed: 0 / 4 exercises
Knowledge Check
Question 1 of 4