Java Generics¶
What are Generics?¶
Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. They provide compile-time type safety and eliminate the need for casting.
// Without generics (pre-Java 5)
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // Manual cast, runtime error if wrong type
// With generics
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // No cast needed, compile-time type checking
// list.add(123); // Compile error!
Generic Classes¶
Basic Generic Class¶
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 value = stringBox.get();
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; }
}
// Usage
Pair<String, Integer> pair = new Pair<>("age", 25);
String key = pair.getKey(); // "age"
Integer value = pair.getValue(); // 25
Generic Class with Bounds¶
// T must extend Number (or be Number)
public class NumericBox<T extends Number> {
private T value;
public NumericBox(T value) {
this.value = value;
}
public double doubleValue() {
return value.doubleValue(); // Safe because T extends Number
}
}
// Usage
NumericBox<Integer> intBox = new NumericBox<>(42);
NumericBox<Double> doubleBox = new NumericBox<>(3.14);
// NumericBox<String> stringBox = ...; // Compile error!
Generic Methods¶
Basic Generic Method¶
public class Util {
// Generic method (note <T> before return type)
public static <T> T getFirst(List<T> list) {
if (list == null || list.isEmpty()) {
return null;
}
return list.get(0);
}
// Generic method with multiple type parameters
public static <K, V> V getValue(Map<K, V> map, K key) {
return map.get(key);
}
}
// Usage - type inference
String first = Util.getFirst(List.of("a", "b", "c"));
Integer num = Util.getFirst(List.of(1, 2, 3));
// Explicit type (rarely needed)
String first2 = Util.<String>getFirst(List.of("a", "b"));
Generic Method with Bounds¶
public class MathUtil {
// T must extend Comparable<T>
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// Multiple bounds (class first, then interfaces)
public static <T extends Number & Comparable<T>> T clamp(T value, T min, T max) {
if (value.compareTo(min) < 0) return min;
if (value.compareTo(max) > 0) return max;
return value;
}
}
// Usage
Integer maxInt = MathUtil.max(10, 20); // 20
String maxStr = MathUtil.max("apple", "banana"); // "banana"
Generic Interfaces¶
public interface Repository<T, ID> {
T findById(ID id);
List<T> findAll();
T save(T entity);
void delete(T entity);
}
// Implementation
public class UserRepository implements Repository<User, Long> {
@Override
public User findById(Long id) { ... }
@Override
public List<User> findAll() { ... }
@Override
public User save(User entity) { ... }
@Override
public void delete(User entity) { ... }
}
Wildcards¶
Unbounded Wildcard (?)¶
// Accepts any type
public void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
// Usage
printList(List.of("a", "b", "c"));
printList(List.of(1, 2, 3));
printList(List.of(new Object()));
// Limitation: Can only read as Object, cannot write (except null)
public void addToList(List<?> list) {
// list.add("hello"); // Compile error!
// list.add(123); // Compile error!
list.add(null); // OK (but rarely useful)
}
Upper Bounded Wildcard (? extends)¶
// Accepts Number or any subclass (Integer, Double, etc.)
public double sumOfList(List<? extends Number> list) {
double sum = 0;
for (Number n : list) {
sum += n.doubleValue();
}
return sum;
}
// Usage
sumOfList(List.of(1, 2, 3)); // List<Integer>
sumOfList(List.of(1.5, 2.5, 3.5)); // List<Double>
sumOfList(List.of(1L, 2L, 3L)); // List<Long>
// PRODUCER - can read, cannot write
public void cannotAdd(List<? extends Number> list) {
Number n = list.get(0); // OK - read as Number
// list.add(1); // Compile error!
// list.add(1.0); // Compile error!
}
Lower Bounded Wildcard (? super)¶
// Accepts Integer or any superclass (Number, Object)
public void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// Usage
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addIntegers(integers); // OK
addIntegers(numbers); // OK
addIntegers(objects); // OK
// CONSUMER - can write, limited read
public void cannotRead(List<? super Integer> list) {
list.add(10); // OK - write Integer
Object obj = list.get(0); // Only read as Object
// Integer i = list.get(0); // Compile error!
}
PECS Principle¶
Producer Extends, Consumer Super
// If you need to READ from a collection → use extends (producer)
// If you need to WRITE to a collection → use super (consumer)
// If you need BOTH → use exact type (no wildcard)
public class Collections {
// copy() reads from src (producer) and writes to dest (consumer)
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}
}
// Example
List<Number> numbers = new ArrayList<>(List.of(1.0, 2.0, 3.0));
List<Integer> integers = List.of(4, 5, 6);
Collections.copy(numbers, integers); // Copies integers into numbers
Type Erasure¶
Generics are implemented through type erasure - generic type information is removed at compile time.
// What you write
public class Box<T> {
private T content;
public T get() { return content; }
}
// What compiler generates (after erasure)
public class Box {
private Object content;
public Object get() { return content; }
}
// With bounds
public class NumericBox<T extends Number> {
private T value;
}
// After erasure
public class NumericBox {
private Number value; // Erased to bound
}
Consequences of Type Erasure¶
// 1. Cannot use instanceof with generic types
// if (list instanceof List<String>) { } // Compile error!
if (list instanceof List<?>) { } // OK
// 2. Cannot create generic arrays
// T[] array = new T[10]; // Compile error!
T[] array = (T[]) new Object[10]; // Workaround (unchecked warning)
// 3. Cannot use primitive types
// List<int> list = ...; // Compile error!
List<Integer> list = new ArrayList<>(); // Use wrapper
// 4. Runtime type is same for all generic instantiations
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass()); // true!
Generic Type Restrictions¶
Cannot Do¶
// 1. Cannot instantiate generic type
public class Container<T> {
// T item = new T(); // Compile error!
// Workaround: Pass Class<T>
public T createInstance(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
}
// 2. Cannot create array of generic type
// T[] items = new T[10]; // Compile error!
// 3. Cannot use primitives
// List<int> list = ...; // Compile error!
// 4. Cannot use static fields with type parameter
public class Box<T> {
// static T value; // Compile error!
// Because static is shared across all instances
}
// 5. Cannot catch or throw generic type
// catch (T exception) { } // Compile error!
Practical Examples¶
Generic Stack¶
public class Stack<E> {
private List<E> elements = new ArrayList<>();
public void push(E item) {
elements.add(item);
}
public E pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}
public E peek() {
if (isEmpty()) {
throw new EmptyStackException();
}
return elements.get(elements.size() - 1);
}
public boolean isEmpty() {
return elements.isEmpty();
}
public int size() {
return elements.size();
}
}
// Usage
Stack<String> stringStack = new Stack<>();
stringStack.push("hello");
stringStack.push("world");
String top = stringStack.pop(); // "world"
Generic DAO Pattern¶
public interface GenericDao<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
T save(T entity);
void delete(T entity);
boolean existsById(ID id);
}
public abstract class AbstractDao<T, ID> implements GenericDao<T, ID> {
protected Class<T> entityClass;
@SuppressWarnings("unchecked")
public AbstractDao() {
// Get actual type argument at runtime (from subclass)
ParameterizedType type = (ParameterizedType) getClass().getGenericSuperclass();
this.entityClass = (Class<T>) type.getActualTypeArguments()[0];
}
@Override
public List<T> findAll() {
String query = "SELECT e FROM " + entityClass.getSimpleName() + " e";
// Execute query...
return null;
}
}
public class UserDao extends AbstractDao<User, Long> {
// Automatically knows entityClass is User
}
Generic Builder Pattern¶
public class Builder<T extends Builder<T>> {
protected String name;
protected int value;
@SuppressWarnings("unchecked")
protected T self() {
return (T) this;
}
public T name(String name) {
this.name = name;
return self();
}
public T value(int value) {
this.value = value;
return self();
}
}
public class AdvancedBuilder extends Builder<AdvancedBuilder> {
private String extra;
public AdvancedBuilder extra(String extra) {
this.extra = extra;
return self();
}
@Override
protected AdvancedBuilder self() {
return this;
}
}
// Fluent chaining works!
AdvancedBuilder builder = new AdvancedBuilder()
.name("test")
.value(42)
.extra("advanced");
Generic Utility Methods¶
public class GenericUtils {
// Swap elements in array
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// Find max in collection
public static <T extends Comparable<? super T>> T max(Collection<T> coll) {
Iterator<T> iterator = coll.iterator();
T max = iterator.next();
while (iterator.hasNext()) {
T next = iterator.next();
if (next.compareTo(max) > 0) {
max = next;
}
}
return max;
}
// Filter collection by predicate
public static <T> List<T> filter(Collection<T> collection, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T item : collection) {
if (predicate.test(item)) {
result.add(item);
}
}
return result;
}
// Convert list type
public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) {
List<R> result = new ArrayList<>();
for (T item : list) {
result.add(mapper.apply(item));
}
return result;
}
}
Common Interview Questions¶
- What is type erasure?
-
Compiler removes generic type info at compile time, replaces with Object (or bound)
-
Why can't you create
new T()? -
Type erasure - T becomes Object at runtime, compiler doesn't know actual type
-
What's the difference between
<?>and<Object>? -
<?>accepts any generic type,<Object>only acceptsList<Object>exactly -
Explain PECS?
-
Producer Extends, Consumer Super - for read use extends, for write use super
-
Can generics be used with primitives?
-
No, must use wrapper classes (Integer, Double, etc.)
-
What's
List<?>vsList<Object>vsList? List<?>: Any type, can only read as ObjectList<Object>: Specifically Object, can read/write ObjectList(raw): No type checking, legacy, avoid