- Understand what generics are and why they're important
- Learn to create and use generic classes and methods
- Master type safety and code reusability with generics
Introduction to Generics in Java
Imagine building a box. Do you build separate boxes for books, toys, and tools? Or one universal box that still knows what's inside? That's what generics solve.
Before Java 5, collections could hold any object. Two big problems: you had to cast everything, and nothing stopped you from putting the wrong type in. Generics give you type safety at compile time.
The Problem: Life Before Generics
The Dangers of Raw Types
Before generics, collections used "raw types"—they could hold any Object:
// Pre-generics code (still compiles but dangerous)
List myList = new ArrayList();
myList.add("Hello"); // Adding a String
myList.add(42); // Adding an Integer - no error!
myList.add(new Date()); // Adding a Date - still no error!
// When retrieving, you must cast - and hope you're right
String first = (String) myList.get(0); // OK
String second = (String) myList.get(1); // ClassCastException at runtime!
The compiler couldn't help you - errors only appeared when the program ran, often in production, often in code far from where the bug was introduced.
Type Safety: Catching Errors Early
With generics, you declare what type a collection holds:
List<String> names = new ArrayList<String>();
names.add("Alice");
names.add("Bob");
// names.add(42); // Compile error! Can't add Integer to List<String>
String first = names.get(0); // No cast needed - compiler knows it's a String
The key benefit: errors are caught at compile time, not runtime. The compiler becomes your safety net.
Understanding Type Parameters
When you see <T> or <E> in Java code, you're looking at a type parameter - a placeholder for a real type that will be specified later.
Common Type Parameter Names
While you can use any name, Java conventions use single uppercase letters:
| Parameter | Meaning | Common Usage |
|---|---|---|
T |
Type | General-purpose type parameter |
E |
Element | Collections (List, Set) |
K |
Key | Maps (Map<K, V>) |
V |
Value | Maps (Map<K, V>) |
N |
Number | Numeric types |
R |
Return type | Function return types |
These aren't special keywords - they're just conventions that make code more readable.
Your First Generic Class
Let's create a simple generic class - a Box that can hold any type of item:
public class Box<T> {
private T contents;
public void put(T item) {
this.contents = item;
}
public T get() {
return contents;
}
public boolean isEmpty() {
return contents == null;
}
}
Here's what's happening:
<T>declares a type parameter named TTis used as the type for thecontentsfieldTis the parameter type forput()and return type forget()
Using the Generic Box
When you create a Box, you specify the actual type:
// A box for Strings
Box<String> stringBox = new Box<>();
stringBox.put("Hello");
String message = stringBox.get(); // No cast needed
// A box for Integers
Box<Integer> numberBox = new Box<>();
numberBox.put(42);
Integer number = numberBox.get(); // No cast needed
// Compile-time safety
stringBox.put(123); // Error! Can't put Integer in Box<String>
The <> (diamond operator, Java 7+) lets the compiler infer the type from the variable declaration.
How Generics Work: Type Erasure
Here's something important to understand: generics only exist at compile time. The compiler uses them to check your code, then erases them, replacing type parameters with Object (or the bound type).
// What you write:
Box<String> box = new Box<>();
box.put("Hello");
String s = box.get();
// What the compiler sees after type erasure:
Box box = new Box();
box.put("Hello");
String s = (String) box.get(); // Compiler inserts the cast
This is called type erasure. It means:
- Generic type information is not available at runtime
- You can't create instances of type parameters (
new T()is illegal) - You can't use
instanceofwith parameterized types
This design was chosen for backward compatibility with pre-generics Java code.
Generic Methods
You can also create generic methods in non-generic classes:
public class Utilities {
// A generic method - <T> declares the type parameter
public static <T> T firstOrNull(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
// Another generic method
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
The <T> before the return type declares that this method has its own type parameter. Usage:
List<String> names = Arrays.asList("Alice", "Bob");
String first = Utilities.firstOrNull(names); // Type inferred as String
Integer[] numbers = {1, 2, 3};
Utilities.swap(numbers, 0, 2); // Type inferred as Integer
Benefits of Generics
1. Type Safety
Errors are caught at compile time, not runtime:
List<String> names = new ArrayList<>();
names.add(123); // Compile error - caught immediately
2. No Casting Required
The compiler handles conversions:
// Without generics
String name = (String) list.get(0); // Manual cast
// With generics
String name = list.get(0); // No cast needed
3. Code Reuse
Write once, use with any type:
Box<String> stringBox = new Box<>();
Box<Integer> intBox = new Box<>();
Box<User> userBox = new Box<>();
// Same Box class works for all types
4. Self-Documenting Code
The type parameter tells you what a collection contains:
Map<String, List<Integer>> scores; // Clear: String keys, Lists of Integers
Common Pitfalls
1. Arrays and Generics Don't Mix Well
// This is illegal - can't create generic arrays
T[] array = new T[10]; // Compile error
// Workaround (with unchecked warning)
@SuppressWarnings("unchecked")
T[] array = (T[]) new Object[10];
2. Primitive Types Not Allowed
List<int> numbers; // Error! Can't use primitives
List<Integer> numbers; // OK - use wrapper class
3. Type Erasure Surprises
// These have the same erasure - can't overload
void process(List<String> list) { }
void process(List<Integer> list) { } // Error!
Generics give you compile-time type safety, eliminate casting, and let one piece of code work with many types. Type parameters like <T> are placeholders that get erased at runtime. Use wrapper classes (Integer, not int) and let the diamond operator <> infer types for you.
