Java Generics
Generics enable you to write flexible, reusable code that works with any type while maintaining compile-time type safety. They eliminate casting and catch type errors before your code runs.
Why Generics?
Before generics (Java 1.4 and earlier), collections stored Object types:
// Without Generics (Pre-Java 5) - UNSAFE
List list = new ArrayList();
list.add("Hello");
list.add(42); // Allowed - any Object!
list.add(new User()); // Also allowed!
// Runtime error - ClassCastException!
String s = (String) list.get(1); // 42 is not a String!
// With Generics (Java 5+) - SAFE
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(42); // Compile Error! Type safety enforced
// list.add(new User()); // Compile Error!
String s = list.get(0); // No casting needed!
Benefits of Generics
| Benefit | Description |
|---|---|
| Type Safety | Errors caught at compile time, not runtime |
| No Casting | No need to cast objects when retrieving from collections |
| Code Reuse | Write one class/method that works with any type |
| Better Readability | Type information visible in the code |
Type Parameters
| Parameter | Convention | Example |
|---|---|---|
T |
Type | Box<T> |
E |
Element (collections) | List<E> |
K |
Key | Map<K, V> |
V |
Value | Map<K, V> |
N |
Number | Calculator<N> |
Generic Classes
// Define a generic class
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
public boolean isEmpty() {
return content == null;
}
}
// Usage - T becomes String
Box<String> stringBox = new Box<>();
stringBox.set("Hello Generics");
String value = stringBox.get(); // No cast needed
// Usage - T becomes Integer
Box<Integer> intBox = new Box<>();
intBox.set(42);
Integer num = intBox.get();
// Multiple type parameters
public 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> pair = new Pair<>("age", 25);
Generic Methods
Methods can have their own type parameters, independent of the class.
public class Utility {
// Generic method - works with any array type
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
// Generic method with return type
public static <T> T getFirst(List<T> list) {
if (list.isEmpty()) {
return null;
}
return list.get(0);
}
// Multiple type parameters
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
// Usage
Integer[] nums = {1, 2, 3};
String[] words = {"a", "b", "c"};
Utility.printArray(nums); // Works with Integer[]
Utility.printArray(words); // Works with String[]
List<String> names = List.of("Alice", "Bob");
String first = Utility.getFirst(names); // Returns "Alice"
Bounded Type Parameters
Restrict the types that can be used with a generic.
// Upper bound: T must be Number or its subclass
public class Calculator<T extends Number> {
private T value;
public Calculator(T value) {
this.value = value;
}
public double doubleValue() {
return value.doubleValue(); // Can call Number methods!
}
}
Calculator<Integer> intCalc = new Calculator<>(42);
Calculator<Double> dblCalc = new Calculator<>(3.14);
// Calculator<String> strCalc = new Calculator<>("x"); // Error!
// Multiple bounds
public <T extends Comparable<T> & Serializable> T findMax(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
Wildcards
Wildcards provide flexibility when the exact type doesn't matter.
// Unbounded wildcard: ? - any type
public void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
// Upper bounded: ? extends Type - read-only
public double sumOfList(List<? extends Number> list) {
double sum = 0;
for (Number n : list) {
sum += n.doubleValue();
}
return sum;
}
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.1, 2.2, 3.3);
sumOfList(ints); // Works!
sumOfList(doubles); // Works!
// Lower bounded: ? super Type - write-only
public void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
List<Number> nums = new ArrayList<>();
List<Object> objs = new ArrayList<>();
addNumbers(nums); // Works!
addNumbers(objs); // Works!
PECS Rule: Producer Extends, Consumer Super.
• Use
• Use
• Use
? extends T when you only read from a collection (producer)• Use
? super T when you only write to a collection (consumer)
Generic Interfaces
// Define a generic interface
public interface Repository<T, ID> {
T findById(ID id);
List<T> findAll();
void save(T entity);
void delete(T entity);
}
// Implement with specific types
public class UserRepository implements Repository<User, Long> {
@Override
public User findById(Long id) { /* ... */ }
@Override
public List<User> findAll() { /* ... */ }
@Override
public void save(User entity) { /* ... */ }
@Override
public void delete(User entity) { /* ... */ }
}
Type Erasure
Java generics use type erasure - generic type information is removed at runtime.
// At compile time
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
// At runtime, both become just List (raw type)
// This means:
strings.getClass() == integers.getClass() // true!
// Limitations due to type erasure:
// - Cannot create generic arrays: new T[10]
// - Cannot use instanceof with generics: obj instanceof List<String>
// - Cannot create instances of type parameters: new T()
Output
Click Run to execute your code
Summary
- Generics provide compile-time type safety and eliminate casting
- Use type parameters:
T(Type),E(Element),K,V(Key, Value) - Bounded types (
extends) restrict allowed types - Wildcards:
?(any),? extends T(read),? super T(write) - PECS: Producer Extends, Consumer Super
- Type erasure: Generic info removed at runtime
Enjoying these tutorials?