By WebNest Studio Engineering Team
Java - Core Tutorial
Generics in Java
Generics let classes, interfaces, and methods operate on a type that is specified as a parameter, rather than being hardcoded or left as raw Object. Introduced in Java 5, generics moved a whole category of bugs from runtime to compile time: without them, a collection could accept any type of object, and pulling the wrong type back out failed only when you tried to use it, often far from where the mistake was made.
Why it matters
Every collection you declare uses generics, and so do most modern Java APIs. They move a whole class of errors from runtime, where they appear as ClassCastException in production, to compile time, where they appear as a red line in the editor. Reading signatures that contain <T>, ? extends and ? super is a required skill for using the standard library and Spring.
Why Generics Prevent ClassCastException
Before generics, List stored plain Object references, so code had to cast every element retrieved from it. If the wrong type had been inserted somewhere, that cast failed at runtime with ClassCastException, often in a completely different part of the program. With List<String>, the compiler enforces at every call site that only Strings can be added, and no cast is needed on retrieval — the error is caught immediately, at compile time, instead of surfacing later as a runtime crash.
Type Erasure
Generic type information exists only at compile time; the compiler checks it, then "erases" it, replacing type parameters with Object (or their bound) in the compiled bytecode. This is why you cannot do new T[10], cannot use instanceof against a generic type parameter directly, and why List<String> and List<Integer> share exactly one .class file at runtime. Type erasure was a deliberate design choice for backward compatibility with pre-generics Java bytecode.
Bounded Type Parameters
A bounded type parameter restricts what types can be substituted. <T extends Number> means T must be Number or one of its subclasses (Integer, Double, and so on), which allows the generic code to call Number's methods (like doubleValue()) on values of type T — something plain <T> would not allow, since an unbounded T is treated as Object.
Wildcards: ? extends and ? super
Wildcards describe an unknown type in a context where you don't need to name it exactly. List<? extends Number> ("upper bounded") accepts a list of Number or any subtype, and is safe to read from (you can only add null to it, per the "Get and Put Principle"). List<? super Integer> ("lower bounded") accepts a list of Integer or any supertype, and is safe to write Integer values into. This distinction is often remembered as PECS: "Producer Extends, Consumer Super."
<T extends Number>— bounded type parameter, restricts T to Number and its subtypes.List<? extends Number>— upper bounded wildcard, safe to read, unsafe to add to.List<? super Integer>— lower bounded wildcard, safe to write Integer into.
Example
A generic method with a bounded type parameter, plus wildcard usage
import java.util.List;
public class GenericsDemo {
// Bounded type parameter: T must be a Number or subtype
static double sumAll(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue(); // safe: T extends Number guarantees this method exists
}
return total;
}
static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
System.out.println("Sum of ints: " + sumAll(ints));
System.out.println("Sum of doubles: " + sumAll(doubles));
System.out.println("Max: " + max(10, 25));
}
}
Sum of ints: 6.0
Sum of doubles: 4.0
Max: 25
Common Mistakes
- Trying to create a generic array directly, such as "new T[10]" — type erasure makes this illegal, requiring workarounds like an Object[] cast.
- Adding elements to a "? extends" wildcard list (other than null), which the compiler rejects because the exact subtype is unknown.
- Using a raw type (e.g. "List list = new ArrayList()" without a type parameter) to avoid a compiler warning, which throws away all compile-time type checking.
- Confusing "? extends" (for reading/producing) with "? super" (for writing/consuming) — remember PECS: Producer Extends, Consumer Super.
Exercise
Write a generic method max that returns the largest element of a list. It should work for any type that can be compared with itself, such as Integer and String.
Expected output: 9 then pear
import java.util.List;
public class GenericMax {
// TODO: declare a generic method max that takes a List<T> and returns a T,
// where T is limited to types that implement Comparable<T>
public static void main(String[] args) {
System.out.println(max(List.of(3, 9, 4)));
System.out.println(max(List.of("apple", "pear", "fig")));
}
}
Show hints
- The type parameter goes before the return type:
static <T extends Comparable<T>> T max(List<T> items). - Start with the first element as the best so far and replace it whenever
item.compareTo(best) > 0.
Show solution
import java.util.List;
public class GenericMax {
static <T extends Comparable<T>> T max(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
public static void main(String[] args) {
System.out.println(max(List.of(3, 9, 4))); // 9
System.out.println(max(List.of("apple", "pear", "fig"))); // pear
}
}
Quick quiz
1. What is the main benefit of List<String> over a raw List?
- A. Adding a non-String is a compile-time error, and no cast is needed when reading
- B. It runs faster
- C. It uses less memory
- D. It can hold primitives
Show answer
A. The compiler checks the element type, so the mistake is found before the program runs.
2. At runtime, do List<String> and List<Integer> have different classes?
- A. Yes
- B. No; both are the same class because of type erasure
- C. Only on Java 8
- D. Only if the lists are empty
Show answer
B. The compiler removes the type arguments. Both lists are plain ArrayList (or whichever implementation) at runtime.
3. A method takes List<? extends Number>. What can it safely do with the list?
- A. Read elements as Number, but not add elements
- B. Add any Number
- C. Add Integer values only
- D. Nothing
Show answer
A. The actual list might be a List of Integer or of Double, so the compiler cannot allow adding either.
Interview questions
What is type erasure?
Show answer
Generic type information exists only at compile time. After checking the types, the compiler replaces each type parameter with its bound, or with Object if it has none, and inserts casts where needed. This kept generic code compatible with older Java. Its consequences are that you cannot write new T(), cannot use instanceof List<String>, and cannot overload two methods that differ only in a type argument.
What is the difference between "? extends T" and "? super T"?
Show answer
List<? extends T> is a list of some unknown subtype of T: you can read elements as T but cannot add to it. List<? super T> is a list of some unknown supertype of T: you can add T values but can read elements only as Object. The usual rule is "producer extends, consumer super": use extends when the method reads from the collection and super when it writes to it.
Why is List<Integer> not a subtype of List<Number>?
Show answer
If it were, a method could receive a List<Integer> as a List<Number> and add a Double to it, which would break the original list. Generic types are therefore not related just because their type arguments are. To accept a list of any kind of number, a method declares its parameter as List<? extends Number>.
Key Points to Remember
- Generics catch type mismatches at compile time, eliminating a whole category of runtime ClassCastException bugs.
- Type erasure removes generic type information after compilation, which is why generic arrays and instanceof checks against type parameters are restricted.
- Bounded type parameters (<T extends Number>) let generic code call methods defined on the bound.
- Wildcards (? extends for reading, ? super for writing) make APIs more flexible without sacrificing type safety.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering