Java Notes
Complete guide to Java Generics — type parameters, bounded types, wildcards, type erasure, generic methods, generic classes, and common patterns with practical examples.
Why Generics?
Before Java 5, collections stored everything as Object. This caused two problems:
- No type safety at compile time — you could add any object to any collection
- Required explicit casting — retrieving elements needed casting, which could fail at runtime
// Pre-generics code (Java 1.4 style)
List names = new ArrayList();
names.add("Alice");
names.add(42); // No compile error — but logically wrong!
String s = (String) names.get(1); // ClassCastException at RUNTIME!Generics solve this by letting you declare what type a collection holds:
// With generics (Java 5+)
List<String> names = new ArrayList<>();
names.add("Alice");
// names.add(42); // COMPILE ERROR — caught early!
String s = names.get(0); // No cast needed!Generic Methods
A method can define its own type parameters, independent of the class:
public class Utility {
// Generic static method
public static <T> T getLastElement(List<T> list) {
if (list.isEmpty()) return null;
return list.get(list.size() - 1);
}
// Generic method with multiple type params
public static <K, V> Map<V, K> reverseMap(Map<K, V> original) {
Map<V, K> reversed = new HashMap<>();
for (Map.Entry<K, V> entry : original.entrySet()) {
reversed.put(entry.getValue(), entry.getKey());
}
return reversed;
}
// Generic method with bounded type
public static <T extends Comparable<T>> T findMax(List<T> list) {
if (list.isEmpty()) throw new IllegalArgumentException("Empty list");
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
}
// Usage — type inference determines T automatically
String last = Utility.getLastElement(Arrays.asList("A", "B", "C")); // "C"
Integer max = Utility.findMax(Arrays.asList(3, 1, 4, 1, 5)); // 5Bounded Type Parameters
Upper Bounded (extends)
Restricts the type parameter to a specific type or its subtypes:
// T must be a Number or subclass of Number
public class NumberBox<T extends Number> {
private T value;
public NumberBox(T value) {
this.value = value;
}
public double doubleValue() {
return value.doubleValue(); // Can call Number methods!
}
}
NumberBox<Integer> intBox = new NumberBox<>(42);
NumberBox<Double> dblBox = new NumberBox<>(3.14);
// NumberBox<String> strBox = new NumberBox<>("hi"); // COMPILE ERROR!Multiple Bounds
// T must implement BOTH Comparable AND Serializable
public <T extends Comparable<T> & Serializable> T findMin(List<T> list) {
return Collections.min(list);
}
// A class can only be one bound (and must be first), interfaces come after
public <T extends Number & Comparable<T>> void process(T item) { }Wildcards
Wildcards (?) represent an unknown type. They are used in variable declarations and method parameters, not in class definitions.
Unbounded Wildcard (?)
// Accepts a list of ANY type
public static void printAll(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
printAll(Arrays.asList(1, 2, 3));
printAll(Arrays.asList("A", "B"));Upper Bounded Wildcard (? extends T) — "Producer"
Accepts T or any subtype of T. You can read from it but cannot write (except null).
// Accepts List<Number>, List<Integer>, List<Double>, etc.
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue(); // Can read as Number
}
// numbers.add(42); // COMPILE ERROR — can't add!
return total;
}
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> dbls = Arrays.asList(1.5, 2.5);
System.out.println(sum(ints)); // 6.0
System.out.println(sum(dbls)); // 4.0Lower Bounded Wildcard (? super T) — "Consumer"
Accepts T or any supertype of T. You can write to it but reading returns Object.
PECS — Producer Extends, Consumer Super
This is the golden rule for wildcard usage:
| If the parameterized type... | Use... | Example |
|---|---|---|
| Produces values (you READ from it) | ? extends T | List<? extends Number> |
| Consumes values (you WRITE to it) | ? super T | List<? super Integer> |
| Both produces and consumes | Exact type T | List<T> |
Real-World PECS Example
Type Erasure
What Happens at Compile Time
Java generics are implemented via type erasure — the compiler:
- Replaces all type parameters with their bounds (or
Objectif unbounded) - Inserts casts where necessary
- Generates bridge methods to preserve polymorphism
// What you write:
public class Box<T> {
private T value;
public T get() { return value; }
public void set(T value) { this.value = value; }
}
// What the compiler generates (after erasure):
public class Box {
private Object value;
public Object get() { return value; }
public void set(Object value) { this.value = value; }
}Consequences of Type Erasure
Generic Interfaces
public interface Repository<T, ID> {
T findById(ID id);
List<T> findAll();
void save(T entity);
void delete(ID id);
}
// Implementation specifies concrete 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(Long id) { /* ... */ }
}Generic Constructors
public class Container<T> {
private T value;
// Generic constructor — U is independent of T
public <U extends T> Container(U value) {
this.value = value;
}
}Recursive Type Bounds
// The type must be comparable to itself
public static <T extends Comparable<T>> T max(Collection<T> collection) {
Iterator<T> it = collection.iterator();
T result = it.next();
while (it.hasNext()) {
T current = it.next();
if (current.compareTo(result) > 0) {
result = current;
}
}
return result;
}
// Enum pattern — every enum implicitly extends Enum<E>
// public abstract class Enum<E extends Enum<E>> { ... }Generics with Collections Framework
// Type-safe collections
Map<String, List<Integer>> scoresByStudent = new HashMap<>();
scoresByStudent.put("Alice", Arrays.asList(95, 88, 92));
scoresByStudent.put("Bob", Arrays.asList(78, 85, 90));
// Computing with type safety
for (Map.Entry<String, List<Integer>> entry : scoresByStudent.entrySet()) {
double avg = entry.getValue().stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
System.out.println(entry.getKey() + ": " + avg);
}Common Patterns
Type-Safe Heterogeneous Container
public class TypeSafeMap {
private Map<Class<?>, Object> map = new HashMap<>();
public <T> void put(Class<T> type, T value) {
map.put(type, value);
}
public <T> T get(Class<T> type) {
return type.cast(map.get(type)); // Safe cast using Class.cast()
}
}
TypeSafeMap container = new TypeSafeMap();
container.put(String.class, "Hello");
container.put(Integer.class, 42);
String s = container.get(String.class); // "Hello" — no cast needed
Integer i = container.get(Integer.class); // 42Generic Builder Pattern
public class Builder<T extends Builder<T>> {
private String name;
@SuppressWarnings("unchecked")
public T withName(String name) {
this.name = name;
return (T) this;
}
}Restrictions and Limitations
| What You Cannot Do | Reason |
|---|---|
new T() | Type unknown at runtime (erasure) |
new T[10] | Cannot create generic array |
instanceof List<String> | Type info erased at runtime |
List<int> | Primitives not allowed (use wrappers) |
static T field | Static members shared across all instances |
class Foo extends Throwable | Cannot create generic exceptions |
Common Mistakes
1. Raw Types
// WRONG — raw type, loses all type safety
List list = new ArrayList();
list.add("hello");
list.add(42); // No warning!
// CORRECT
List<String> list = new ArrayList<>();2. Confusing List<Object> and List<?>
// List<Object> — you can add Objects
// List<?> — you CANNOT add anything (except null)
public void process(List<?> list) {
// list.add("hello"); // COMPILE ERROR
// Can only read (returns Object)
Object first = list.get(0);
}3. Heap Pollution
// Varargs + generics can cause heap pollution
@SafeVarargs // Suppress warning if method is safe
public static <T> List<T> asList(T... elements) {
List<T> list = new ArrayList<>();
for (T e : elements) list.add(e);
return list;
}Interview Questions
Q1: What are generics and why were they introduced?
A: Generics provide compile-time type safety for collections and eliminate the need for explicit casts. They were introduced in Java 5 to prevent ClassCastException at runtime.
Q2: What is type erasure?
A: The Java compiler removes all generic type information at compile time, replacing type parameters with Object (or their bound). This ensures backward compatibility with pre-generics code.
Q3: What is the difference between List<?>, List<Object>, and List<? extends Object>?
A: List<Object> — can add Object and read Object. List<?> — can't add anything (read-only). List<? extends Object> — same as List<?>. Key difference: List<String> IS-A List<?> but IS-NOT-A List<Object>.
Q4: Explain PECS.
A: Producer Extends, Consumer Super. If a method reads from a parameterized collection, use ? extends T. If it writes to it, use ? super T.
Q5: Can you create a generic array in Java?
A: Not directly. new T[10] won't compile. Workaround: (T[]) new Object[10] with an unchecked cast warning, or use Array.newInstance(clazz, size).
Q6: Why can't primitives be used as type arguments?
A: Because generics work with Object references after type erasure. Primitives aren't objects. Use wrapper classes (Integer, Double, etc.) with autoboxing.
Q7: What is the difference between <T extends Number> and <? extends Number>?
A: <T extends Number> declares a type parameter for a class or method. <? extends Number> is a wildcard for a variable/parameter declaration — you don't name the type, just constrain it.
Q8: What is type erasure in Java?
A: Type erasure is the process by which the Java compiler removes all generic type information at compile time, replacing type parameters with their bounds (or Object if unbounded). This means List<String> and List<Integer> are the same class at runtime — you cannot use instanceof with generic types or create generic arrays.
Q9: Why can't you create a generic array like new T[]?
A: Due to type erasure, the JVM doesn't know the actual type T at runtime. Arrays need to know their component type for runtime type checking (ArrayStoreException). Since generic type info is erased, creating new T[] would be unsafe. Use ArrayList<T> instead.
Q10: What is the difference between <? extends T> and <? super T>?
A: <? extends T> (upper bounded) means "any type that is T or a subtype of T" — you can READ from it as T but cannot WRITE (except null). <? super T> (lower bounded) means "any type that is T or a supertype of T" — you can WRITE T into it but reads return Object. This is the PECS principle: Producer Extends, Consumer Super.
Q11: Can you overload methods that differ only in generic type parameter?
A: No. Due to type erasure, void process(List<String> list) and void process(List<Integer> list) both become void process(List list) after erasure, causing a compile error for duplicate methods.
Q12: What are reifiable types in Java?
A: Reifiable types are types whose full type information is available at runtime (not erased). Examples: primitives, non-generic classes, raw types, unbound wildcards (List<?>), arrays of reifiable types. Non-reifiable types include List<String>, T, List<? extends Number> — their type info is partially erased.
Summary
| Concept | Purpose |
|---|---|
<T> | Declare a type parameter |
<T extends X> | Bounded type parameter |
<?> | Unbounded wildcard |
<? extends T> | Upper bounded wildcard (read) |
<? super T> | Lower bounded wildcard (write) |
| PECS | Producer Extends, Consumer Super |
| Type Erasure | Generics exist only at compile time |
Generics are the foundation of type-safe collections in Java. Master them to write reusable, safe, and expressive code.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Generics in Java.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Java Master Course topic.
Search Terms
java-master-course, java master course, java, master, course, collections, framework, generics
Related Java Master Course Topics