Introduction
Generics are a powerful feature introduced in Java 5 that enable type-safe programming by allowing classes, interfaces, and methods to operate on objects of various types while providing compile-time type checking. Generics eliminate the need for explicit casting and prevent ClassCastException at runtime by ensuring that only the correct types of objects are stored and retrieved. They are widely used in the Java Collections Framework (e.g., ArrayList<String>, HashMap<Integer, String>) and are essential for writing reusable, robust, and maintainable code.
1. Why Use Generics?
Problem Without Generics
// Raw ArrayList (pre-Java 5 style)
List list = new ArrayList();
list.add("Hello");
list.add(123); // Accidentally added an Integer
String s = (String) list.get(1); // Runtime ClassCastException!
Solution With Generics
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // Compile-time error!
String s = list.get(0); // No cast needed
Benefits of Generics
- Type safety: Catches errors at compile time.
- No explicit casting: Cleaner, more readable code.
- Code reuse: Write algorithms that work with multiple types.
- Better documentation: Type parameters clarify intended usage.
2. Generic Classes
A generic class declares one or more type parameters in angle brackets (<>).
Syntax
class ClassName<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
Example: Generic Box
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
// Usage
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String s = stringBox.get(); // No cast
Box<Integer> intBox = new Box<>();
intBox.set(42);
Integer n = intBox.get();
Note: Type parameters are compile-time only—erased at runtime (type erasure).
3. Generic Methods
Methods can also be generic, independent of the class.
Syntax
public <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
Example
public class Util {
public static <T> boolean contains(T[] array, T item) {
for (T element : array) {
if (element.equals(item)) {
return true;
}
}
return false;
}
}
// Usage
String[] names = {"Alice", "Bob", "Charlie"};
boolean found = Util.contains(names, "Bob"); // true
Note: The type
Tis inferred from the arguments—no need to specify it explicitly.
4. Bounded Type Parameters
Restrict the types that can be used as type arguments.
A. Upper Bounded Wildcards (extends)
// Accepts Number or its subclasses (Integer, Double, etc.)
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue();
}
return total;
}
// Usage
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5);
System.out.println(sum(ints)); // OK
System.out.println(sum(doubles)); // OK
B. Lower Bounded Wildcards (super)
// Accepts String or its superclasses (Object, CharSequence, etc.)
public static void addStrings(List<? super String> list) {
list.add("Hello");
list.add("World");
}
PECS Principle (Producer-Extends, Consumer-Super)
- Producer (provides data): Use
? extends T - Consumer (accepts data): Use
? super T
5. Wildcards
Wildcards (?) represent an unknown type.
Types of Wildcards
| Wildcard | Meaning | Example |
|---|---|---|
? | Unbounded wildcard | List<?> — list of unknown type |
? extends T | Upper bounded | List<? extends Number> |
? super T | Lower bounded | List<? super Integer> |
Example: Unbounded Wildcard
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
Use Case: When the method only uses functionality from
Object(e.g.,toString()).
6. Type Erasure
Generics are implemented using type erasure:
- Type parameters are removed during compilation.
- Bytecode contains only raw types (e.g.,
Listinstead ofList<String>). - Casts are inserted automatically where needed.
Consequences
- Cannot create instances of type parameters:
T elem = new T(); // ❌ Compilation error
- Cannot use
instanceofwith generic types:
if (list instanceof List<String>) { } // ❌ Invalid
- Array creation not allowed:
T[] arr = new T[10]; // ❌ Invalid
Workaround: Pass a
Class<T>object for reflection:T[] arr = (T[]) Array.newInstance(clazz, size);
7. Common Generic Types in Java Collections
| Collection | Generic Form | Description |
|---|---|---|
List | List<E> | Ordered collection (e.g., ArrayList<String>) |
Set | Set<E> | Unique elements (e.g., HashSet<Integer>) |
Map | Map<K, V> | Key-value pairs (e.g., HashMap<String, Integer>) |
Queue | Queue<E> | FIFO structure (e.g., LinkedList<String>) |
8. Best Practices
- Use generics everywhere possible—especially with collections.
- Prefer bounded wildcards for flexible APIs (
? extends T,? super T). - Don’t use raw types (e.g.,
Listinstead ofList<String>)—they bypass type safety. - Use meaningful type parameter names:
E– Element (e.g.,List<E>)K– Key (e.g.,Map<K, V>)V– ValueT– TypeS,U,V– Additional types- Avoid unchecked warnings—they indicate potential runtime errors.
9. Common Mistakes
- Assuming
List<String>is a subtype ofList<Object>:
List<Object> list = new ArrayList<String>(); // ❌ Compilation error
Fix: Use wildcards:
List<? extends Object> list = new ArrayList<String>();
- Trying to add to a list with upper-bounded wildcard:
List<? extends Number> list = new ArrayList<Integer>(); list.add(42); // ❌ Not allowed (could be List<Double>)
- Ignoring compiler warnings about unchecked operations.
10. Practical Example: Generic Repository
public class Repository<T> {
private List<T> items = new ArrayList<>();
public void save(T item) {
items.add(item);
}
public T findById(int index) {
return items.get(index);
}
public List<T> findAll() {
return new ArrayList<>(items);
}
}
// Usage
Repository<User> userRepo = new Repository<>();
userRepo.save(new User("Alice"));
User u = userRepo.findById(0);
Conclusion
Generics are a cornerstone of modern Java programming, enabling type-safe, reusable, and maintainable code. By catching type errors at compile time and eliminating the need for casts, they significantly reduce runtime failures and improve code clarity. Understanding how to define generic classes and methods, use bounded wildcards, and apply the PECS principle allows developers to write flexible and robust APIs. While type erasure imposes some limitations, these are outweighed by the safety and expressiveness generics provide. Whether working with collections, building frameworks, or designing domain models, mastering generics is essential for professional Java development. Always prefer generics over raw types—they are not just a convenience, but a critical tool for correctness.