Using Generics
What Are Generics?
Generics allow you to write classes, interfaces, and methods that work with type parameters — placeholders resolved at compile time. They provide type safety without casting and eliminate a whole class of ClassCastException errors at runtime.
java// Without generics — requires cast, no compile-time safety
List raw = new ArrayList();
raw.add("hello");
String s = (String) raw.get(0); // cast required
// With generics — no cast, compiler enforces the type
List<String> typed = new ArrayList<>();
typed.add("hello");
String s2 = typed.get(0); // no cast neededGeneric Classes
A generic class declares one or more type parameters in angle brackets after the class name. By convention, single uppercase letters are used: T (type), E (element), K / V (key/value), N (number).
javapublic class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T getValue() { return value; }
public void setValue(T value){ this.value = value; }
}
Box<String> strBox = new Box<>("OCP");
Box<Integer> intBox = new Box<>(21);
System.out.println(strBox.getValue()); // OCP
System.out.println(intBox.getValue()); // 21Multiple type parameters are separated by commas:
javapublic 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; }
}
Pair<String, Integer> p = new Pair<>("age", 30);Generic Methods
A method can declare its own type parameter independently of the enclosing class. The type parameter list appears before the return type.
javapublic class Utils {
public static <T> T identity(T value) {
return value;
}
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
}
String s = Utils.identity("Java");
int n = Utils.max(10, 20); // 20
String hi = Utils.max("Apple", "Banana"); // "Banana"Generic Interfaces
Interfaces can also be parameterized. An implementing class either provides a concrete type or remains generic.
javapublic interface Container<T> {
void add(T item);
T get(int index);
}
// Concrete implementation — provides specific type
public class StringContainer implements Container<String> {
private List<String> list = new ArrayList<>();
public void add(String item) { list.add(item); }
public String get(int index) { return list.get(index); }
}
// Generic implementation — stays parameterized
public class GenericContainer<T> implements Container<T> {
private List<T> list = new ArrayList<>();
public void add(T item) { list.add(item); }
public T get(int index) { return list.get(index); }
}Bounded Wildcards
Wildcards (?) represent an unknown type and appear only in variable declarations, parameters, and return types — not in class or method type parameter lists.
Unbounded Wildcard <?>
Accepts a collection of any type. You can only read elements as Object.
javapublic static void printAll(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
printAll(List.of(1, 2, 3));
printAll(List.of("a", "b"));Upper-Bounded Wildcard <? extends T>
Accepts T or any subtype of T. Useful for reading from a collection. You cannot add elements (except null) because the exact subtype is unknown.
javapublic static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
return total;
}
sum(List.of(1, 2, 3)); // works — Integer extends Number
sum(List.of(1.5, 2.5, 3.0)); // works — Double extends NumberLower-Bounded Wildcard <? super T>
Accepts T or any supertype of T. Useful for writing into a collection. You can add elements of type T safely.
javapublic static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
List<Number> nums = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(nums); // OK — Number is a supertype of Integer
addNumbers(objects); // OK — Object is a supertype of IntegerProducer Extends, Consumer Super (PECS)
A helpful mnemonic for choosing the right wildcard:
| Role | Wildcard | Example |
|---|---|---|
| Reading (producer) | <? extends T> |
sum(List<? extends Number>) |
| Writing (consumer) | <? super T> |
addNumbers(List<? super Integer>) |
| Both read and write | No wildcard — use <T> |
Generic method with <T> |
Type Erasure
Generic type information exists only at compile time. The compiler replaces all type parameters with their bounds (or Object if unbounded) and inserts casts as needed. This process is called type erasure.
java// At compile time
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);
// After erasure (conceptually what the JVM sees)
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);Key consequences of type erasure:
- You cannot use
instanceofwith a parameterized type:list instanceof List<String>does not compile. - You cannot create an array of a parameterized type:
new T[10]is not allowed. - Generic type information is not available at runtime via reflection on instances.
Exam Tips
- A wildcard
?cannot be used as a type parameter when declaring a generic class or method — it only appears as a type argument. List<Integer>is not a subtype ofList<Number>even thoughIntegerextendsNumber. UseList<? extends Number>to accept both.- An unbounded wildcard
<?>is equivalent to<? extends Object>. - Lower-bounded wildcards (
<? super T>) allow adding elements of typeTbut reading only gives backObject. - Type parameters are erased at runtime; you cannot call
new T()directly without aClass<T>token.