Chapter 4: Generics
effective-java generics java best-practices
Book: Effective Java, 3rd Edition — Joshua Bloch
Status: 🟩 Complete
Difficulty: Hard
Items: 26-33 (8 items)
Time to complete: ~45 min
Overview
Chapter 4 covers generics — the parameterized type system introduced in Java 5. Generics allow type-safe containers and algorithms, catching errors at compile time rather than at runtime with ClassCastException. The chapter’s central challenge is understanding erasure: generic type information is erased at runtime, which creates a set of restrictions (no generic arrays, raw types still exist) and surprises (heap pollution, unchecked warnings). Mastering generics means knowing when to use type parameters, when to use wildcards (? extends T vs. ? super T), and how to write APIs that are both flexible for callers and type-safe. Java 17 brings no major generics changes, but var (Java 10+) reduces verbosity in local variable declarations.
Items
Item 26: Don’t use raw types
The Problem
A raw type is a generic type used without a type parameter (e.g., List instead of List<String>). Raw types exist only for compatibility with pre-Java-5 code. Using them loses all the type safety benefits of generics and defers errors to runtime.
// BAD: raw type — errors at runtime, not compile time
private final Collection stamps = new ArrayList();
stamps.add(new Coin()); // Compiles! No warning in old code, warning in new code
// ClassCastException at runtime, not at the line where Coin was added
for (Iterator i = stamps.iterator(); i.hasNext(); ) {
Stamp stamp = (Stamp) i.next(); // BOOM: ClassCastException
stamp.cancel();
}The Solution
Always parameterize generic types. Use the unbounded wildcard type <?> if you genuinely don’t know or care about the type parameter.
// GOOD: parameterized — error caught at compile time
private final Collection<Stamp> stamps = new ArrayList<>();
stamps.add(new Coin()); // COMPILE ERROR: type mismatch — cannot add Coin to Collection<Stamp>
for (Stamp stamp : stamps) {
stamp.cancel(); // no cast needed, always correct
}
// If you don't know the type: use wildcard, not raw type
static int numElementsInCommon(Set<?> s1, Set<?> s2) {
int result = 0;
for (Object o1 : s1)
if (s2.contains(o1)) result++;
return result;
}Why This Works
Parameterized types allow the compiler to enforce type constraints. ClassCastException moves from runtime to compile time — a guaranteed improvement. Set<?> (unbounded wildcard) is safe: you cannot add anything except null to a Set<?>, which prevents heap pollution.
Legitimate exceptions to avoiding raw types:
- Class literals:
List.classis legal;List<String>.classis not. instanceof:if (o instanceof Set)— type parameters cannot be used withinstanceofbecause they are erased at runtime.
// CORRECT: use raw type with instanceof, then cast to wildcard
if (o instanceof Set) {
Set<?> s = (Set<?>) o; // safe: wildcard, not raw type
}When to Apply / When NOT to Apply
- Always use parameterized types in new code.
- Use raw types only in the two exceptions above.
- Raw types in APIs are a code smell indicating old code that needs migration.
Java 17 Update
No generics changes. However, var (Java 10+) infers the parameterized type locally, reducing verbosity without losing type safety:
var stamps = new ArrayList<Stamp>(); // type inferred as ArrayList<Stamp>Item 27: Eliminate unchecked warnings
The Problem
Unchecked warnings indicate potential ClassCastException at runtime due to type erasure. They are easy to dismiss with @SuppressWarnings("unchecked") — but doing so carelessly hides real bugs.
// BAD: careless suppression hides the real problem
@SuppressWarnings("unchecked") // applied to entire method — too broad
public <T> T[] toArray(T[] a) {
if (a.length < size) {
return (T[]) Arrays.copyOf(elements, size, a.getClass());
}
// ... rest of method
}The Solution
Eliminate every unchecked warning possible. When you CANNOT eliminate a warning and the cast is provably safe, suppress it on the smallest possible scope and add a comment explaining WHY it is safe.
// GOOD: suppress on the single assignment, not the whole method
public <T> T[] toArray(T[] a) {
if (a.length < size) {
// This cast is correct because the array we're creating is of the same type
// as the one passed in, which is itself of type T[].
@SuppressWarnings("unchecked")
T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
return result;
}
System.arraycopy(elements, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}Common unchecked warnings and their causes:
| Warning | Cause |
|---|---|
| Unchecked cast | Casting to a generic type: (List<String>) obj |
| Unchecked method invocation | Calling a method with raw type arguments |
| Unchecked conversion | Assigning a raw type to a parameterized type |
| Unchecked vararg creation | See Item 32 |
When to Apply / When NOT to Apply
- Every unchecked warning should be addressed — either fixed or suppressed with justification.
@SuppressWarnings("unchecked")on a class is almost always wrong.-Xlint:uncheckedflag injavacshows detailed unchecked warning information.
Java 17 Update
No change. Compile with -Xlint:unchecked and -Werror in CI to treat unchecked warnings as errors.
Item 28: Prefer lists to arrays
The Problem
Arrays and generics have fundamentally different type rules that interact badly. Arrays are covariant (if Sub is a subtype of Super, then Sub[] is a subtype of Super[]). Generics are invariant (List<Sub> is NOT a subtype of List<Super>). Arrays also reify their element type at runtime, while generics use erasure.
// BAD: arrays are covariant — runtime failure instead of compile-time failure
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit"; // Throws ArrayStoreException at runtime
// This is caught at compile time for lists:
List<Object> ol = new ArrayList<Long>(); // COMPILE ERROR — incompatible types// BAD: generic array creation is illegal — but raw array + cast "works" with heap pollution risk
// This compiles but is unsafe:
List<String>[] stringLists = (List<String>[]) new List[1]; // unchecked cast
List<Integer> intList = List.of(42);
Object[] objects = stringLists; // legal — array covariance
objects[0] = intList; // OK at runtime (no ArrayStoreException — erasure!)
String s = stringLists[0].get(0); // ClassCastException at runtime!The Solution
Use List<E> instead of E[] when you need to hold generic elements. The performance cost is usually negligible compared to the safety benefit.
// BAD: array-based generic stack — cannot be done safely
public class Stack<E> {
private E[] elements;
private int size = 0;
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[16]; // unchecked cast
}
// ...
}
// GOOD: list-based — no unchecked warnings, fully type-safe
public class Stack<E> {
private final List<E> elements = new ArrayList<>();
private int size = 0;
public void push(E e) { elements.add(e); size++; }
public E pop() {
if (size == 0) throw new EmptyStackException();
E e = elements.get(--size);
elements.set(size, null); // eliminate obsolete reference
return e;
}
}Choosing between lists and arrays:
// When returning multiple values of generic type — use a list
public static <E> List<E> pickTwo(E a, E b, E c) {
return switch (ThreadLocalRandom.current().nextInt(3)) {
case 0 -> List.of(a, b);
case 1 -> List.of(a, c);
default -> List.of(b, c);
};
// Compare to array version which requires unchecked cast or Object[]
}When to Apply / When NOT to Apply
- Prefer
List<E>overE[]in generic classes. - Arrays are still appropriate for performance-sensitive code with known, fixed element types (e.g.,
int[]for numeric algorithms). - Use arrays for
varargsparameters (see Item 32 for the interaction).
Java 17 Update
List.of(), List.copyOf(), and List.from() (Java 9-10+) make creating immutable lists concise. var reduces declaration verbosity. No changes to the fundamental arrays-vs-generics tension.
Item 29: Favor generic types
The Problem
Client code that uses a non-generic class must cast results, which is both verbose and dangerous.
// BAD: non-generic stack — requires clients to cast
public class Stack {
private Object[] elements;
private int size = 0;
public Stack() { elements = new Object[16]; }
public void push(Object e) { elements[size++] = e; }
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
}
// Client must cast:
Stack stack = new Stack();
stack.push("hello");
String s = (String) stack.pop(); // unchecked cast — could fail at runtime
// With a non-generic stack, you lose all type safety:
stack.push(42);
String bad = (String) stack.pop(); // ClassCastException at runtime!The Solution
Make the class generic by adding a type parameter.
// GOOD: generic stack — safe, no client-side casts needed
public class Stack<E> {
private Object[] elements; // cannot do E[] directly — see Item 28
private int size = 0;
private static final int DEFAULT_CAPACITY = 16;
@SuppressWarnings("unchecked")
public Stack() {
// Safe: elements is private, only E instances are ever added
elements = (E[]) new Object[DEFAULT_CAPACITY]; // Option 1
// Alternative Option 2: use Object[] and cast in pop():
// elements = new Object[DEFAULT_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
@SuppressWarnings("unchecked")
public E pop() {
if (size == 0) throw new EmptyStackException();
// Option 2: cast here instead
E result = (E) elements[--size];
elements[size] = null; // eliminate obsolete reference
return result;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
public boolean isEmpty() { return size == 0; }
}
// Client code — no casts:
Stack<String> stack = new Stack<>();
stack.push("hello");
String s = stack.pop(); // type-safe, no castTwo approaches for the generic array problem:
- Option 1: Cast the entire array to
E[]in the constructor. Suppress warning once, but the unchecked cast is less visible. - Option 2: Use
Object[]and cast individual elements inpop(). More casts, but the unchecked warning is closer to where the runtime failure would occur.
When to Apply / When NOT to Apply
- Generify new collection-like classes from the start.
- When generifying existing classes, ensure the change is backward-compatible (raw type usage still compiles with warnings).
- Bounded type parameters:
class DelayQueue<E extends Delayed>— elements must implementDelayed.
Java 17 Update
var reduces declaration verbosity: var stack = new Stack<String>(). No change to the generic implementation strategy.
Item 30: Favor generic methods
The Problem
Non-generic utility methods require clients to cast arguments and return values, losing type safety.
// BAD: non-generic method — raw types, unchecked warning, unsafe
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1); // unchecked warning
result.addAll(s2); // unchecked warning
return result;
}
// Client must cast and can mix types unsafely:
Set strings = Set.of("hello", "world");
Set numbers = Set.of(1, 2, 3);
Set mixed = union(strings, numbers); // compiles — but semantically wrongThe Solution
Add a type parameter to the method. Type parameters are declared in angle brackets before the return type.
// GOOD: generic method — type-safe, no client casts
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
// Client: type-safe, type-inferred
Set<String> guys = Set.of("Tom", "Dick", "Harry");
Set<String> stooges = Set.of("Larry", "Moe", "Curly");
Set<String> aflCio = union(guys, stooges); // no cast, fully type-safeGeneric singleton factory (for stateless generic objects):
// A single stateless function object reused for any type T
private static final UnaryOperator<Object> IDENTITY_FN = t -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
return (UnaryOperator<T>) IDENTITY_FN; // safe: IDENTITY_FN never modifies its argument
}
// Usage:
UnaryOperator<String> sameString = identityFunction();
UnaryOperator<Number> sameNumber = identityFunction();Recursive type bound — type parameter bounded by an expression involving itself:
// T must be comparable to itself — natural ordering
public static <T extends Comparable<T>> T max(List<T> list) {
if (list.isEmpty()) throw new IllegalArgumentException("Empty list");
T result = list.get(0);
for (int i = 1; i < list.size(); i++)
if (list.get(i).compareTo(result) > 0)
result = list.get(i);
return result;
}When to Apply / When NOT to Apply
- Generify utility methods that operate on generic types (sets, lists, maps).
- Use bounded type parameters when the method requires a specific capability of the type.
- If the return type depends on the input types, use a generic method.
Java 17 Update
var works with generic method return types in local variables. No change to generic method syntax.
Item 31: Use bounded wildcards to increase API flexibility
The Problem
Parameterized types are invariant: List<String> is not a subtype of List<Object>, and List<Number> is not a subtype of List<Integer>. This makes APIs that work with a family of related types unnecessarily restrictive.
// BAD: too restrictive — only works with Stack<Number>, not Stack<Integer>
public void pushAll(Iterable<E> src) {
for (E e : src) push(e);
}
Stack<Number> stack = new Stack<>();
Iterable<Integer> ints = List.of(1, 2, 3);
stack.pushAll(ints); // COMPILE ERROR: Integer is not Number for invariant typesThe Solution
Apply the PECS principle: Producer Extends, Consumer Super.
- If a parameter produces T values (you read from it), use
<? extends T>. - If a parameter consumes T values (you write to it), use
<? super T>.
// GOOD: PECS — producer uses extends
// src produces E values for the stack to consume
public void pushAll(Iterable<? extends E> src) {
for (E e : src) push(e);
}
// GOOD: PECS — consumer uses super
// dst consumes E values that the stack produces
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) dst.add(pop());
}
// Now works correctly:
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> ints = List.of(1, 2, 3);
numberStack.pushAll(ints); // Integer extends Number — OK
Collection<Object> objects = new ArrayList<>();
numberStack.popAll(objects); // Object is super of Number — OKDetailed PECS example — the max() method:
// Without wildcards — too restrictive
public static <T extends Comparable<T>> T max(List<T> list)
// With PECS — more flexible
// list is a producer of T (we read from it)
// Comparable is a consumer of T (it compares T values against a T argument)
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
if (list.isEmpty()) throw new IllegalArgumentException("Empty list");
T result = list.get(0);
for (int i = 1; i < list.size(); i++)
if (list.get(i).compareTo(result) > 0)
result = list.get(i);
return result;
}
// The wildcard version handles:
List<ScheduledFuture<?>> scheduledFutures = ...;
// ScheduledFuture implements Delayed, Delayed implements Comparable<Delayed>
// Without "? super T" in Comparable bound, this would fail
max(scheduledFutures); // Works! T = ScheduledFuture, Comparable<? super T> = Comparable<Delayed>The helper method pattern — when wildcards complicate the implementation, move logic to a private generic helper:
// Public API with wildcards — easy to call
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j); // delegate to private generic helper
}
// Private generic helper — can capture the type to remove the wildcard
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}PECS cheat sheet:
| Scenario | Use |
|---|---|
| Reading from a structure | ? extends T |
| Writing to a structure | ? super T |
| Both reading and writing | T (no wildcard) |
| Don’t care about type at all | ? (unbounded) |
When to Apply / When NOT to Apply
- Apply PECS whenever a method takes a parameterized type for input or output.
- Do NOT use wildcards in return types — it forces callers to also use wildcards.
- Do NOT apply wildcards when the method reads AND writes to the same parameterized argument.
Java 17 Update
No change to wildcards. However, var can capture wildcard types locally when you need to interact with a List<?> and call set() (similar to the swap helper pattern).
Item 32: Combine generics and varargs judiciously
The Problem
Varargs create an array of the arguments under the hood. Since generic arrays cannot be safely created (Item 28), combining varargs with generic types creates a heap pollution hazard — a variable of a parameterized type refers to an object that is not of that type.
// BAD: heap pollution via generic varargs
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists; // legal: array covariance
objects[0] = intList; // heap pollution: stringLists[0] is now a List<Integer>
String s = stringLists[0].get(0); // ClassCastException at runtime!
}The Solution
If you write a method with generic varargs that is safe (does not store anything into the varargs array, does not expose the array to untrusted code), annotate it with @SafeVarargs to suppress the caller-side warning.
// GOOD: safe generic varargs — annotated appropriately
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> flatten(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
// Safe because:
// 1. We never store anything INTO the varargs array
// 2. We never expose the varargs array reference to untrusted code
List<String> result = flatten(List.of("a","b"), List.of("c","d"));The safest alternative: List instead of varargs:
// EVEN BETTER: replace varargs with List — no @SafeVarargs needed, fully typesafe
@SuppressWarnings("unchecked")
public static <T> List<T> flatten(List<List<? extends T>> lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
// Client: slightly more verbose but completely safe
List<String> result = flatten(List.of(List.of("a","b"), List.of("c","d")));Rules for @SafeVarargs:
- The method must NOT write to the varargs array.
- The method must NOT export the array to code that is not trusted.
- Only use on methods that cannot be overridden (
static,final, orprivate). @SafeVarargscan only be applied to methods (not constructors in Java 8, but allowed in Java 9+).
When to Apply / When NOT to Apply
- Use
@SafeVarargson your own generic varargs methods when they are provably safe. - Prefer
List<T>overT...when dealing with generic varargs in new APIs. - Do not suppress warnings you haven’t verified.
Java 17 Update
No change. List.of(...) (Java 9+) is the idiomatic way to pass multiple generic elements where you would previously use varargs.
Item 33: Consider typesafe heterogeneous containers
The Problem
Standard generic containers (List<E>, Map<K,V>) are parameterized on the container, limiting you to one type per container. Sometimes you want a container that holds values of many different types — for example, a row in a database (column name → value of any type).
// BAD: raw types — loses all type safety
Map<String, Object> row = new HashMap<>();
row.put("name", "Alice");
row.put("age", 30);
String name = (String) row.get("name"); // unchecked cast — could fail
Integer age = (Integer) row.get("age"); // unchecked castThe Solution
Instead of parameterizing the container, parameterize the key. Use Class<T> as the key — this is called a typesafe heterogeneous container or type token pattern.
// GOOD: typesafe heterogeneous container
public class Favorites {
private final Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type)); // dynamic cast — safe!
}
}
// Usage: each type has its own slot, fully type-safe
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 42);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class); // no cast needed
Integer favoriteInteger = f.getFavorite(Integer.class); // no cast neededThe Class.cast() method is the key: it is the dynamic analog of (T) but uses the Class<T> token to perform a checked cast at runtime, throwing ClassCastException with a useful message if it fails (rather than at an unpredictable point downstream).
Limitation 1: raw type clients can break the container:
// Malicious client can bypass type safety with raw Class:
f.putFavorite((Class) Integer.class, "Not an Integer"); // no compile error
Integer x = f.getFavorite(Integer.class); // ClassCastException!Fix: validate the instance in putFavorite:
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(type, type.cast(instance)); // throws ClassCastException if type mismatch
}Limitation 2: cannot use with generic types (non-reifiable types):
List<String>.class is a syntax error — List<String> and List<Integer> erase to the same Class object (List.class). You can only use reifiable types as keys. The Guava TypeToken library solves this via super type tokens.
Bounded type tokens — restricting which types can be stored:
// Only Class objects that represent annotation types are accepted
public <T extends Annotation> T getAnnotation(Class<T> annotationType) {
return annotationType.cast(annotationMap.get(annotationType));
}
// asSubclass() converts a Class<?> to a Class<? extends T> safely at runtime:
public <T extends Annotation> T getAnnotation(
AnnotatedElement element, String annotationTypeName) {
Class<?> annotationType = null;
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation(
annotationType.asSubclass(Annotation.class));
}When to Apply / When NOT to Apply
- Use the typesafe heterogeneous container pattern when you need a container that maps types to instances of those types.
- Common applications: plugin systems, attribute maps, dependency injection containers, database row representations.
- Avoid when non-reifiable types (like
List<String>) need to be keys — use GuavaTypeTokenor a different design.
Java 17 Update
No direct change, but records often serve as the typed value in such containers, making the pattern more ergonomic. Pattern matching for instanceof (Java 16+) can reduce boilerplate when extracting values:
// Java 16+: pattern matching reduces casting boilerplate
Object value = favorites.get(type);
if (value instanceof String s) {
System.out.println("String: " + s.toUpperCase());
}Interview Questions & Exercises
Q1: What is type erasure and what problems does it cause?
Context: Every senior Java interview, generics deep-dive questions.
Answer: Type erasure is the process by which the Java compiler removes generic type information from the compiled bytecode. At runtime, List<String> and List<Integer> are both just List. The compiler inserts casts where needed, and the bytecode is identical to pre-generics code.
Problems caused by erasure:
- Cannot use
instanceofwith parameterized types:x instanceof List<String>is a compile error. - Cannot create generic arrays:
new E[]is illegal. - Cannot create instances of type parameters:
new T()is illegal. - Raw types still exist for backward compatibility, allowing heap pollution.
- Overloading on generic types is impossible:
void process(List<String> l)andvoid process(List<Integer> l)have the same erasure — compile error.
// Cannot overload — both erase to process(List)
void process(List<String> l) { } // COMPILE ERROR
void process(List<Integer> l) { }Follow-up: “How does the compiler ensure type safety if generics are erased?” The compiler inserts checked casts at the bytecode level wherever a value is extracted from a generic container. These casts are guaranteed to succeed if the code is free of unchecked warnings.
Q2: Explain PECS with a concrete, non-trivial example.
Context: FAANG-level Java interviews, Spring/library design discussions.
Answer: PECS stands for Producer Extends, Consumer Super. The rule: use ? extends T when a parameterized argument produces T values (you read from it), and ? super T when it consumes T values (you write to it).
// Copy utility: src produces T, dst consumes T
public static <T> void copy(List<? extends T> src, List<? super T> dst) {
for (T item : src)
dst.add(item);
}
List<Integer> ints = List.of(1, 2, 3);
List<Number> numbers = new ArrayList<>();
copy(ints, numbers); // Integer extends Number — works!
// Without PECS, you'd need:
// copy(List<T> src, List<T> dst) — would require both to be exactly the same type
// copy(ints, numbers) would fail — Integer != NumberThe intuition: if you’re reading (producing) values into your method, the actual type can be any subtype of T — hence ? extends T. If you’re writing (consuming) values from your method into the parameter, the actual type needs to be able to hold a T — meaning it can be T or any supertype — hence ? super T. If you do both, use T directly (no wildcard).
Follow-up: “Should wildcards appear in return types?” No — if a method returns List<? extends T>, callers cannot add anything to the list. Wildcards in return types force wildcards on the caller side, creating an API that’s harder to use.
Q3: Why can’t you create a generic array? What are the alternatives?
Context: Java internals, collections design, generics interviews.
Answer: Generic array creation is prohibited because it would undermine the type system. Arrays are covariant and reified — they check types at runtime. Generics are invariant and erased — they only check types at compile time. Combining them creates a gap: you could use array covariance to store the wrong type into a generic array without an ArrayStoreException, and the resulting ClassCastException would appear far from the source of the bug.
// If this were legal (it's not):
List<String>[] arr = new List<String>[1]; // hypothetically legal
Object[] obj = arr; // legal — array covariance
obj[0] = List.of(42); // no ArrayStoreException — erasure hides the type mismatch
String s = arr[0].get(0); // ClassCastException here, not where the bug was introducedAlternatives:
- Use
List<E>instead ofE[](preferred for generic classes — Item 28). - Cast
new Object[]toE[]and suppress the warning with justification (acceptable in private fields of generic classes). - Use
(E[]) Array.newInstance(clazz, length)with aClass<E>parameter when you need a real array of the specific component type.
Follow-up: “When is the Object-array-cast approach (Option 2) preferable to the List approach (Option 1)?” When performance matters for primitive-adjacent types (no boxing), or when you must interoperate with APIs that require arrays.
Q4: What is heap pollution and when does it occur?
Context: Deep Java generics questions, senior/staff engineer interviews.
Answer: Heap pollution occurs when a variable of a parameterized type refers to an object that is not of that type. It arises when raw types or unchecked casts are used. At the moment of heap pollution, there is no exception — the ClassCastException occurs later, when a value is extracted and the hidden cast (inserted by the compiler at erasure) fails.
The most common cause in modern code is generic varargs methods:
static void pollute(List<String>... lists) {
Object[] arr = lists; // array covariance: legal
arr[0] = List.of(42); // no ArrayStoreException — erasure
String s = lists[0].get(0); // ClassCastException here!
}Detection: any unchecked cast or unchecked method invocation is a potential source of heap pollution. Eliminate unchecked warnings or use @SafeVarargs only when you’ve verified safety.
Follow-up: “What is @SafeVarargs and when can you apply it?” It suppresses the caller-side warning for a generic varargs method. You may apply it only when the method (a) never writes to the varargs array and (b) never exposes the array to untrusted code. It can only be applied to methods that cannot be overridden.
Q5: What is the typesafe heterogeneous container pattern and where is it used in the JDK?
Context: Plugin systems, advanced API design, senior Java interviews.
Answer: The pattern parameterizes the key rather than the container, using Class<T> as the key. The container is Map<Class<?>, Object>, and values are retrieved via type.cast(map.get(type)), which performs a dynamic (runtime-checked) cast.
JDK uses:
AnnotatedElement.getAnnotation(Class<A> annotationType)— retrieves a specific annotation by type.Class.cast(Object obj)— the building block of the pattern.ServiceLoader(conceptually) — loads service implementations parameterized on a service type.
// JDK example — AnnotatedElement:
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
// Usage:
Override ann = method.getAnnotation(Override.class); // type-safe, no castLimitation: Only works with reifiable types. List<String>.class doesn’t exist — List<String> and List<Integer> share List.class. Guava’s TypeToken addresses this with super type tokens.
Follow-up: “How would you implement a type-safe attribute bag for a plugin system?” Use Class<T> keys, type.cast() for retrieval, and type.cast(instance) in the put method for fail-fast validation.
Q6: What is the difference between <? extends T> and <T extends SomeClass>?
Context: Common generics confusion, junior-to-mid-level Java interviews.
Answer:
<T extends SomeClass>is a bounded type parameter on a method or class declaration. It declares a new type variableTand constrains it to beSomeClassor a subtype. The caller specifies T and it is fixed for the call. You can useTto refer to the type in multiple places.<? extends SomeClass>is a bounded wildcard in a type argument position. It means “some unknown type that extends SomeClass.” You cannot name the type — it is anonymous. You cannot write to aList<? extends SomeClass>(exceptnull) because the compiler cannot verify what the actual type is.
// Bounded type parameter: T is named and usable
public static <T extends Comparable<T>> T max(List<T> list) { ... }
// Bounded wildcard: unknown type, read-only
public static double sumNumbers(List<? extends Number> list) {
return list.stream().mapToDouble(Number::doubleValue).sum();
// Cannot do: list.add(new Integer(5)); — type unknown
}Follow-up: “When would you use a bounded wildcard over a bounded type parameter?” When you only need to READ from the parameterized argument, or when you want the method signature to be more flexible (allowing, e.g., List<Integer> where T extends Number would also accept List<Number> but only that exact type).
Q7: Why does the compiler allow List<String> to be passed to a method taking List<Object> in some cases but not others?
Context: Generics invariance confusion — very common interview topic.
Answer: This is the invariance of parameterized types. List<String> is NOT a subtype of List<Object>, even though String is a subtype of Object. This is by design: if it were allowed, you could do:
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // hypothetically legal
objects.add(42); // adds Integer to what is actually a List<String>
String s = strings.get(0); // ClassCastExceptionThe compiler prevents List<String> from being passed to List<Object>. However:
List<String>CAN be passed toList<? extends Object>(wildcard — read-only).List<String>CAN be passed toList<?>(unbounded wildcard — read-only).List<Object>CAN be passed toList<? super String>(can add Strings, read as Object).
Arrays DO allow this substitution (String[] is a subtype of Object[]), but fail with ArrayStoreException at runtime — a design flaw that generics fix.
Follow-up: “What is the technical term for the relationship between List<String> and List<Object>?” They are unrelated types — neither is a subtype of the other. This is called invariance.
Q8: When should you prefer List<T> over T[] in a generic class?
Context: Generic data structure design interviews, Java collections internals.
Answer: Prefer List<E> over E[] in generic classes unless you have a strong performance reason for arrays. Reasons:
- Type safety:
List<E>is fully type-safe at compile time.E[]requires an unchecked cast (either in the constructor or in every accessor). - No heap pollution risk: Arrays are covariant, which means an
E[]can be assigned to anObject[]and then written to with the wrong type —List<E>cannot. - Richer API:
List<E>gives youaddAll,subList, streams, etc. - Simpler code: No
@SuppressWarnings("unchecked")needed.
Use arrays when:
- Performance is critical and boxing overhead matters (use
int[],long[], etc.). - You’re implementing the innermost layer of a high-performance data structure and you want full control over memory layout.
- You need to return a typed array from a generic method (unavoidable —
toArray(T[] a)pattern).
Follow-up: “How does ArrayList handle this internally?” ArrayList uses an Object[] internally and casts elements to E in get() with @SuppressWarnings("unchecked"). It accepts the unchecked cast because it controls all insertions and guarantees only E instances are ever added.
Key Takeaways
- Never use raw types in new code — they exist only for backward compatibility. Use
<?>(unbounded wildcard) if you genuinely don’t care about the type. - Eliminate every unchecked warning — each one is a potential
ClassCastException. Suppress only with@SuppressWarnings("unchecked")on the smallest possible scope and a comment explaining why the cast is safe. - Lists over arrays in generic code — arrays are covariant and reified; generics are invariant and erased. Mixing them unsafely enables heap pollution.
- Generify your classes and methods — it is better than requiring clients to cast, and it moves errors from runtime to compile time.
- PECS: Producer Extends, Consumer Super — the golden rule for choosing wildcards. Apply to every parameterized method argument; never apply to return types.
- Generic varargs requires
@SafeVarargs— generic varargs methods create arrays that can cause heap pollution. Annotate safe methods; preferList<T>for truly safe alternatives. - Typesafe heterogeneous containers — when you need a map from types to instances of those types, parameterize the key (
Class<T>) rather than the container. Usetype.cast()for runtime-safe retrieval. - Type erasure is fundamental — you cannot use
instanceofwith parameterized types, cannot create generic arrays, and cannot overload on generic type arguments. Design around these constraints. var(Java 10+) reduces verbosity —var map = new HashMap<String, List<Integer>>()infers the full generic type. Use it for local variables to reduce noise without losing type safety.- See also: ch03-classes-and-interfaces for generic static member classes and records, ch05-enums-and-annotations for annotation type tokens used with
getAnnotation().
Last Updated: 2026-05-10