[!NOTE] Generics were added in Java 5 to provide compile-time type checking and remove the risk of
ClassCastExceptionthat plagued early developers. Understanding Generics is what separates junior developers from mid-level engineers.
Life before Generics
Before Generics, a standard Java ArrayList simply stored raw Object references. You could throw anything into it:
ArrayList myCart = new ArrayList(); // No type declared!
myCart.add("Apple"); // String
myCart.add(450); // Integer
myCart.add(new Car()); // Car Object
// To retrieve the Apple, you had to manually "cast" it, praying it was actually a String.
String fruit = (String) myCart.get(0);
// If you accidentally grabbed index 1, your entire app crashed violently at Runtime!
String broken = (String) myCart.get(1); // ClassCastException: Cannot cast Integer to String
Life with Generics
Generics fix this by forcing you to specify the Type Parameter inside angle brackets <> .
// The compiler now strictly enforces that ONLY Strings enter this list!
ArrayList<String> myCart = new ArrayList<>();
myCart.add("Apple");
// myCart.add(450); // COMPILER ERROR! Refuses to run!
String fruit = myCart.get(0); // No manual casting required!
Creating Your Own Generic Class
You can use Generics in your own classes to create ultra-reusable architecture. Instead of hardcoding a type like String, you use a placeholder letter (conventionally T for Type).
// 1. We declare 'T' as a flexible placeholder when we write the blueprint.
public class Box<T> {
private T contents;
public void setContents(T item) {
this.contents = item;
}
public T getContents() {
return this.contents;
}
}
public class Main {
public static void main(String[] args) {
// 2. We physically lock in the Type specifically when we instantiate the Box!
Box<Integer> integerBox = new Box<Integer>();
integerBox.setContents(42);
Box<String> stringBox = new Box<String>();
stringBox.setContents("Secret Document");
}
}
[!CAUTION] Type Erasure: You cannot use primitive types like
intinside angle brackets because Generics actively erase their type boundaries at runtime (a concept called Type Erasure) to maintain backwards compatibility with 1990s Java code. You MUST use wrapper classes like<Integer>!
Generics Make APIs Safer
Generics are not only for collections. They let you design reusable classes and methods while keeping compile-time type safety. A good generic API lets callers choose the type while the compiler prevents accidental mixing.
Generic Method Example
static <T> T first(List<T> items) {
if (items.isEmpty()) {
throw new IllegalArgumentException("List cannot be empty");
}
return items.get(0);
}
The same method works for List<String>, List<Integer>, or any other object type, while still returning the correct type.
Bounded Type Parameters
static <T extends Number> double sum(List<T> numbers) {
double total = 0;
for (T number : numbers) {
total += number.doubleValue();
}
return total;
}
Common Mistakes
- Using raw types such as
Listinstead ofList<String>. - Trying to use primitives directly, such as
List<int>. - Adding too many type parameters and making APIs hard to read.
- Expecting generic type information to always be available at runtime.
Mini Practice
Create a generic Pair<K, V> class with two fields and getters. Use it once for student name and marks, and once for product id and price.
Practice Lab: Generic Pair
Build a reusable generic class with two type parameters.
- Create a class
Pair<K, V>. - Add private final fields for key and value.
- Add a constructor and getters.
- Create
Pair<String, Integer>for student name and marks. - Create
Pair<Integer, String>for product id and product name.
Goal: Practice reusable type-safe code without raw casts.
Revision Checkpoint
- Generics: Add compile-time type safety.
- Type parameter: Placeholder such as
T,K, orV. - Raw type: Avoid plain
Listbecause it loses safety. - Wrapper types: Use
Integer, notint, inside generics. - Type erasure: Generic type details are mostly removed at runtime.
Before the quiz: Explain why List<String> prevents a later cast error.