List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");Java Generics: Type Safety, Type Erasure, and Wildcards (? extends / super)
Direct Answer & Core Concept
Java Generics allow developers to write classes, interfaces, and methods that work with different types while maintaining compile-time type safety.
Instead of storing everything as Object and manually casting values, generics let you specify the expected type directly. This makes code easier to understand and helps the compiler detect many type-related mistakes before the program runs.
In this tutorial, you will learn Java generics, generic classes, generic methods, bounded type parameters, wildcards, ? extends, ? super, type erasure, raw types, and common generic programming mistakes.
Detailed Explanation & Mechanics
What Are Generics in Java?
Generics allow a type to be supplied as a parameter to a class, interface, or method.
For example:
Here, String is the type argument.
The declaration:
List<String>means that the list is intended to contain String values.
The compiler can therefore detect an invalid operation such as:
names.add(100);This gives a compile-time error instead of allowing an incorrect value to enter the collection.
Why Were Generics Introduced?
Before generics, collections commonly stored values as Object.
For example:
List names = new ArrayList();
names.add("Alice");
names.add("Bob");When retrieving a value, a cast was required:
String name =(String) names.get(0);This approach has several problems.
The compiler cannot provide the same level of type checking, and an incorrect value can lead to a ClassCastException later.
With generics:
List<String> names = new ArrayList<>();
String name = names.get(0);The cast is normally unnecessary because the compiler knows the list's element type.
Generics Provide Compile-Time Type Safety
One of the biggest advantages of generics is compile-time type checking.
Consider:
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add("Thirty");The last statement is rejected by the compiler because "Thirty" is a String, while the list accepts Integer values.
This moves many errors from runtime to compile time.
Without GenericsObject↓Collection↓Cast↓Possible runtime errorWith GenericsType parameter↓Compile-time checking↓Safer codeGeneric Collection Example
Generics are heavily used throughout the Java Collections Framework.
List<String> names = new ArrayList<>();
Set<Integer> numbers = new HashSet<>();
Map<Integer, String> users = new HashMap<>();The type arguments describe what each collection contains.
For example:
Map<Integer, String>means:
Key -> IntegerValue -> StringGeneric Classes
You can create your own generic class.
class Box<T> {
private T value;
public void set(T value) {
this.value = value;
} public T get() {
return value;
}
}Here, T is a type parameter.
You can create a Box for different types:
Box<String> nameBox = new Box<>();
nameBox.set("Java");
String name = nameBox.get();Or:
Box<Integer> numberBox = new Box<>();
numberBox.set(100);
Integer number = numberBox.get();The same class works with different types.
What Does T Mean?
T does not have any special magical meaning to the compiler.
It is simply a conventional name for a type parameter.
Common conventions include:
| Type Parameter | Common Meaning |
|---|---|
T | Type |
E | Element |
K | Key |
V | Value |
N | Number |
R | Result |
For example:
class Pair<K, V> {
private K key;
private V value;
}K and V are simply type parameter names.
Multiple Type Parameters
A generic class can have multiple type parameters.
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<Integer, String> user = new Pair<>(101, "Alice");
System.out.println(user.getKey());
System.out.println(user.getValue());Here:
K = IntegerV = StringGeneric Methods
A method can also have its own type parameter.
public static <T> void printValue(T value) {
System.out.println(value);
}You can call it with different types:
printValue("Hello");
printValue(100);
printValue(10.5);The compiler can infer the appropriate type in many cases.
Generic Method Syntax
The general form is:
public static <T> T methodName(T value) {
return value;
}The <T> before the return type declares the method's type parameter.
For example:
public static <T> T identity(T value) {
return value;
}Usage:
String text = identity("Java");
Integer number = identity(100);Generic Interfaces
Interfaces can also use type parameters.
interface Repository<T> {
void save(T value);
T find();
}A class can implement the interface with a specific type.
class UserRepository implements Repository<String> {
private String value;
public void save(String value) {
this.value = value;
} public String find() {
return value;
}
}The generic interface defines a reusable type-safe contract.
Bounded Type Parameters
Sometimes you do not want to accept every possible type.
You can restrict a generic type using a bound.
For example:
public static <T extends Number> double square(T value) {
return value.doubleValue() * value.doubleValue();
}Here:
T extends Numbermeans that T must be Number or a subclass of Number.
Examples:
square(10);
square(10.5);Both Integer and Double satisfy the bound because they extend Number.
Multiple Bounds
A type parameter can have multiple bounds.
For example:
<T extends Number & Comparable<T>>The class bound, if present, must appear first, followed by interface bounds.
Example:
public static <T extends Number & Comparable<T>> void process(T value) {
// ...
}This requires the type to satisfy both constraints.
Generic Types Are Invariant
One of the most important concepts in Java generics is invariance.
Suppose:
List<Integer> integers = new ArrayList<>();You might think this should be allowed:
List<Number> numbers = integers;But Java does not allow it.
Even though:
Integer extends Numberit does not follow that:
List<Integer> extends List<Number>This prevents type-safety problems.
Why Is List<Integer> Not a List<Number>?
Imagine Java allowed:
List<Integer> integers = new ArrayList<>();
List<Number> numbers = integers;Then this would become possible:
numbers.add(3.14);But the original list is supposed to contain only Integer objects.
That would break type safety.
Therefore Java keeps generic parameterized types invariant.
Wildcards in Java
A wildcard represents an unknown type.
The basic wildcard syntax is:
?For example:
List<?> list;This means:
A list of some unknown type.
The exact type is unknown to the code using the wildcard.
Unbounded Wildcard: ?
Consider:
public static void printList(List<?> list) {
for (Object value : list) {
System.out.println(value);
}
}This method can accept:
List<String>List<Integer>List<Double>and many other parameterized lists.
The wildcard makes the method flexible without using a raw type.
What Can You Add to List<?>?
Consider:
List<?> list = new ArrayList<String>();You generally cannot add an arbitrary value:
list.add("Hello");This is not allowed because the actual type represented by ? is unknown.
The only generally safe value to add is null:
list.add(null);You can read values from List<?> as Object:
Object value = list.get(0);Upper-Bounded Wildcard: ? extends
The syntax:
? extends Typemeans an unknown type that is Type or a subtype of Type.
For example:
List<? extends Number>can represent:
List<Integer>or:
List<Double>or:
List<Float>Example of ? extends
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number number : numbers) {
total += number.doubleValue();
} return total;
}Now you can pass:
List<Integer> integers = List.of(10, 20, 30);
List<Double> decimals = List.of(1.5, 2.5);
System.out.println(sum(integers));
System.out.println(sum(decimals));The method can read the elements as Number.
Why Can't You Add to ? extends?
Consider:
List<? extends Number> numbers;The actual list could be:
List<Integer>If Java allowed:
numbers.add(3.14);that would attempt to put a Double into a possible List<Integer>.
Therefore Java prevents adding arbitrary non-null values.
This is the key idea:
With
? extends, you can safely read values as the upper-bound type, but you generally cannot add values.
? extends Is Useful for Producers
A useful rule is:
PECS — Producer Extends, Consumer Super
If a generic structure produces values for your code to read, ? extends is often appropriate.
Example:
public static void printNumbers( List<? extends Number> numbers) {
for (Number number : numbers) {
System.out.println(number);
}
}The list produces Number values for the method.
Lower-Bounded Wildcard: ? super
The syntax:
? super Typemeans an unknown type that is Type or a supertype of Type.
For example:
List<? super Integer>can refer to:
List<Integer>List<Number>or:
List<Object>Example of ? super
public static void addNumbers( List<? super Integer> list) {
list.add(10);
list.add(20);
list.add(30);
}This is safe because every possible type represented by:
? super Integercan accept an Integer.
Why Can You Add to ? super?
Suppose:
List<? super Integer> list;The actual list could be:
List<Integer>or:
List<Number>or:
List<Object>All of these can safely store an Integer.
Therefore:
list.add(100);is allowed.
What Do You Get From ? super?
Reading from:
List<? super Integer>is more restricted.
You can safely retrieve values as Object:
Object value = list.get(0);But Java cannot guarantee that the returned object is an Integer.
For example, if the actual list is:
List<Number>it could contain a Double as well.
Therefore this is not generally safe:
Integer value = list.get(0);? super Is Useful for Consumers
Following PECS:
Consumer Super
If your method is putting values into a generic structure, ? super can often be used.
Example:
public static void addUsers( List<? super User> users) {
users.add(new User());
}The method is consuming User objects by adding them to the list.
PECS: Producer Extends, Consumer Super
PECS is one of the most useful rules for understanding Java wildcards.
Producer -> ? extendsConsumer -> ? superProducer
If you mainly read values:
List<? extends Number>Consumer
If you mainly add values:
List<? super Integer>This is a guideline rather than a rule that every generic method must follow.
extends vs super
The difference can be summarized like this:
| Wildcard | Meaning | Read | Add |
|---|---|---|---|
? | Unknown type | As Object | Only null |
? extends T | Unknown subtype of T | As T | Generally no non-null value |
? super T | Unknown supertype of T | As Object | T values are safe |
Example:
List<? extends Number>is useful when reading numbers.
List<? super Integer>is useful when adding integers.
Generic Arrays
Java treats arrays and generics differently.
Arrays are covariant:
Integer[] integers = new Integer[10];
Number[] numbers = integers;This is allowed.
But generics are invariant:
List<Integer> integers = new ArrayList<>();
// Not allowed
// List<Number> numbers = integers;This difference is important when designing APIs.
Why Generic Array Creation Is Restricted
You cannot normally write:
List<String>[] lists = new List<String>[10];Java does not allow direct creation of arrays of most parameterized types.
However, you may encounter unchecked operations involving generic arrays in advanced code.
This restriction exists because arrays retain runtime component-type information, while generic type arguments are subject to type erasure.
Type Erasure in Java
Java generics were designed to provide compile-time type safety while maintaining compatibility with older Java code.
One important mechanism is type erasure.
During compilation, generic type information is largely removed from the generated bytecode's ordinary runtime type representation.
For example:
List<String> names = new ArrayList<>();and:
List<Integer> numbers = new ArrayList<>();are both based on the runtime class:
java.util.ArrayListThe JVM does not normally have separate runtime classes such as:
ArrayList<String>ArrayList<Integer>Simple Type Erasure Example
Consider:
class Box<T> {
private T value;
public void set(T value) {
this.value = value;
} public T get() {
return value;
}
}Conceptually, after type erasure, the implementation is similar to using Object for an unbounded type parameter:
class Box {
private Object value;
public void set(Object value) {
this.value = value;
} public Object get() {
return value;
}
}The compiler then inserts appropriate casts where necessary.
This is a conceptual explanation rather than a literal source transformation you should expect to see.
Type Erasure With Bounds
If a type parameter has a bound:
<T extends Number>the erased type is generally the leftmost bound.
For example:
class Calculator<T extends Number> {
T value;
}The effective erased type for T is Number.
Why Can't You Use instanceof List<String>?
Because generic type arguments are erased at runtime.
This is invalid:
if (value instanceof List<String>) {
// ...
}The JVM cannot distinguish between:
List<String>and:
List<Integer>using ordinary runtime generic type information.
You can check the raw parameterized class:
if (value instanceof List<?>) {
// ...
}This is allowed because List<?> does not claim a specific element type.
Why Can't You Create new T()?
This is not allowed:
class Box<T> {
T create() {
return new T();
}
}At runtime, due to type erasure, the JVM does not generally know which concrete type T represents.
A common alternative is to pass a factory, constructor reference, or Class<T> when appropriate.
For example:
class Box<T> {
private final Supplier<T> factory;
Box(Supplier<T> factory) {
this.factory = factory;
} T create() {
return factory.get();
}
}Usage:
Box<StringBuilder> box = new Box<>(StringBuilder::new);Generic Type Information at Runtime
Consider:
List<String> names = new ArrayList<>();You cannot generally ask the list:
"What exact generic type argument are you storing?"and expect the ordinary object instance to provide String.
The runtime object is an ArrayList.
Generic information can sometimes be available through reflection on class or method declarations, but that is different from the runtime object knowing the concrete type argument of every instance.
Raw Types
A raw type is a generic type used without specifying its type argument.
For example:
List list = new ArrayList();instead of:
List<String> list = new ArrayList<>();Raw types are mainly retained for backward compatibility with pre-generics Java code.
New code should generally avoid them.
Problems With Raw Types
Consider:
List list = new ArrayList();
list.add("Java");
list.add(100);Now the collection contains different types.
A later cast can fail:
String value =(String) list.get(1);This can result in:
ClassCastExceptionUsing generics prevents this class of mistake earlier:
List<String> list = new ArrayList<>();Unchecked Warnings
You may see compiler warnings such as:
unchecked conversionunchecked callunchecked castThese warnings often indicate that the compiler cannot fully verify type safety.
For example:
List rawList = new ArrayList();
List<String> names = rawList;The compiler may issue an unchecked conversion warning.
Rather than suppressing the warning immediately, investigate why it occurs.
@SuppressWarnings("unchecked")
Sometimes unchecked operations are unavoidable in carefully designed low-level code.
You may encounter:
@SuppressWarnings("unchecked")However, this annotation should be used narrowly.
Do not use it simply to hide generic type-safety problems.
A better approach is to isolate the unchecked operation and ensure the surrounding code maintains the required type invariant.
Generic Inheritance
A class can implement or extend a generic type with a concrete argument.
class StringRepository implements Repository<String> {
@Override public void save(String value) {
System.out.println(value);
} @Override public String find() {
return "Java";
}
}It is also possible to keep the type parameter:
class RepositoryImpl<T> implements Repository<T> {
public void save(T value) {
// ...
} public T find() {
return null;
}
}Generic Constructors
Constructors can use the class's type parameters.
class Box<T> {
private T value;
Box(T value) {
this.value = value;
}
}Usage:
Box<String> box = new Box<>("Java");Constructors can also have their own independent type parameters in certain contexts.
Diamond Operator
Java provides the diamond operator:
<>It allows the compiler to infer generic type arguments.
Instead of:
List<String> names = new ArrayList<String>();you can write:
List<String> names = new ArrayList<>();This is shorter and usually easier to read.
Generic Type Inference
Java can infer type arguments from context.
For example:
var names = new ArrayList<String>();or:
List<String> names = new ArrayList<>();The compiler understands that the ArrayList should be parameterized with String.
Generic methods can also benefit from type inference:
String value = identity("Java");Static Members and Class Type Parameters
A static field cannot use a class's type parameter.
This is invalid:
class Box<T> {
static T value;
}The reason is that static members belong to the class rather than to a particular parameterized instance.
For example:
Box<String>Box<Integer>would share the same static member, so there is no single T that could represent both.
A static method can declare its own type parameter:
static <T> T identity(T value) {
return value;
}Generic Exception Classes
Java does not allow a generic class to directly extend Throwable.
For example, this is not allowed:
class MyException<T> extends Exception {
}Exception handling relies on runtime exception types, while generic type parameters are subject to erasure.
Wildcard Capture
Java sometimes internally treats a wildcard as a specific but unknown type.
For example:
List<?> listcan be thought of as:
List<CAP>where CAP is an unknown captured type.
This concept becomes useful when understanding why certain wildcard operations are allowed or rejected.
For most beginner and intermediate code, however, the practical rules are more important than the internal terminology.
A Practical extends Example
Suppose you want to calculate the total of any numeric list:
public static double total( List<? extends Number> values) {
double result = 0;
for (Number value : values) {
result += value.doubleValue();
} return result;
}Now:
List<Integer> integers = List.of(10, 20, 30);
List<Double> doubles = List.of(1.5, 2.5);
System.out.println(total(integers));
System.out.println(total(doubles));One method works with both collections.
A Practical super Example
Suppose you want a method that copies integers into another collection.
public static void addIntegers( List<? super Integer> destination) {
destination.add(10);
destination.add(20);
destination.add(30);
}It can work with:
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();All can safely receive Integer values.
Collections.copy() and Wildcards
The Java Collections API provides a useful real-world example of wildcard design.
A simplified form of the method is conceptually:
copy( List<? super T> destination, List<? extends T> source)The source produces values of type T.
The destination consumes values of type T.
This directly demonstrates PECS:
Source -> ? extends TDestination -> ? super TCommon Generics Mistakes
Mistake 1: Using Raw Types
Avoid:
List list = new ArrayList();Prefer:
List<String> list = new ArrayList<>();Mistake 2: Assuming Generic Types Are Covariant
This does not compile:
List<Integer> integers = new ArrayList<>();
List<Number> numbers = integers;Use an appropriate wildcard when your API requires variance:
List<? extends Number> numbers = integers;Mistake 3: Adding to ? extends
This is not allowed:
List<? extends Number> numbers = ...;
// numbers.add(10);The exact subtype is unknown.
Mistake 4: Expecting ? super to Return the Exact Type
This is not generally safe:
List<? super Integer> list = ...;
// Integer value = list.get(0);Read values as Object unless you have additional information.
Mistake 5: Confusing extends With Class Inheritance Here
In:
List<? extends Number>extends describes an upper bound on an unknown type argument.
It does not mean that List itself is a subclass of Number.
Generic Method vs Wildcard Method
These two concepts can look similar but solve different problems.
Generic method:
static <T> T first(List<T> list) {
return list.get(0);
}Wildcard method:
static Object first(List<?> list) {
return list.get(0);
}The generic method preserves the relationship between the input and output type.
The wildcard method intentionally treats the element type as unknown.
When Should You Use Generics?
Use generics when:
A class should work with multiple types.
A method should operate on different types safely.
A collection has a known element type.
You want compile-time type checking.
You want to avoid unnecessary casts.
You are designing reusable APIs.
Example:
class Repository<T> {
// ...
}This can support:
Repository<User>Repository<Product>Repository<Order>without duplicating the class for every type.
Generics vs Object
Without generics:
Object value = "Java";
String text =(String) value;With generics:
Box<String> box = new Box<>();
box.set("Java");
String text = box.get();Generics provide a stronger compile-time contract.
Benefits of Java Generics
1. Type Safety
The compiler catches many invalid type operations.
2. Fewer Casts
Generic collections usually return the declared type directly.
3. Reusable Code
One generic class or method can work with many types.
4. Better API Design
Type parameters communicate intended usage clearly.
5. Improved Maintainability
Explicit types make code easier to understand and refactor.
Java Generics Cheat Sheet
<T>Generic type parameterList<T>List containing TList<?>List containing an unknown typeList<? extends T>List of T or a subtype of TList<? super T>List of T or a supertype of TAnd the PECS rule:
Producer -> extendsConsumer -> superGeneric Type Relationships
A useful mental model is:
Number / \ Integer Double | | List<Integer> | X List<Number>Generic collections are invariant.But:
List<? extends Number>can accept a List<Integer> or List<Double>.
Similarly:
List<? super Integer>can accept a List<Integer>, List<Number>, or List<Object>.
Frequently Asked Questions
What are generics in Java?
Generics allow types to be parameterized so that classes, interfaces, and methods can operate on different types while maintaining compile-time type safety.
Why are Java generics useful?
They reduce incorrect type usage, minimize explicit casts, improve code reuse, and make APIs clearer.
What does T mean in Java generics?
T is a conventional name for a type parameter. It is not a reserved keyword and could be replaced with another valid identifier.
What does ? mean in Java?
? represents an unknown type. It is called an unbounded wildcard.
What does ? extends Number mean?
It represents an unknown type that is Number or a subtype of Number.
What does ? super Integer mean?
It represents an unknown type that is Integer or a supertype of Integer.
What is PECS in Java?
PECS stands for Producer Extends, Consumer Super. It is a common guideline for choosing between ? extends and ? super.
Can I add values to List<? extends Number>?
You generally cannot add a non-null value because the actual list could be a list of a specific subtype such as Integer.
Can I add an Integer to List<? super Integer>?
Yes. Every possible type represented by ? super Integer can safely store an Integer.
What is type erasure?
Type erasure is the mechanism by which Java implements generics while maintaining compatibility with older Java bytecode. Generic type arguments are largely removed from the runtime representation, with the compiler inserting necessary casts and enforcing type rules.
Why can't I use instanceof List<String>?
Because the runtime normally cannot distinguish the generic type arguments String and Integer after type erasure. instanceof List<?> is allowed.
Why can't I create new T()?
Because the concrete runtime type represented by an unbounded type parameter is not generally available due to type erasure. A factory or constructor reference can be used instead.
Are generics available for primitive types?
Java generics work with reference types, not primitive types.
This does not work:
List<int> numbers;Use the wrapper type:
List<Integer> numbers;Autoboxing converts between int and Integer when appropriate.
Graphics and Design Ideas
Graphic 1: Java Generics Overview
Java Generics | +---------+---------+ | | | Type Safety Reuse Flexibility | | Compile Time Wildcards | +--------+--------+ | | ? extends ? superGraphic 2: PECS Diagram
Generic Collection | +---------+---------+ | | PRODUCER CONSUMER | | ? extends ? super | | READ WRITEGraphic 3: Type Erasure
Show:
Source Code | vList<String>Box<Integer> | vJava Compiler | vType Checking + Cast Insertion | vRuntime RepresentationGraphic 4: Generic Type Relationships
Number / \Integer DoubleList<Integer> X List<Number>But:List<? extends Number> ↑ accepts both Integer / DoubleGraphic 5: extends vs super
? extends Number
Number
├── Integer
├── Double
└── Float
READ
↓
? super Integer
Object
↑
Number
↑
Integer
WRITE IntegerThese graphics would work well as simple educational diagrams near the corresponding sections and can make the wildcard rules much easier for beginners to remember.
Runnable Code Example: Java Generics
Java 17+import java.util.List;
public class GenericsDemo {
// PECS: Producer Extends (read-only sum)
public static double sumOfList(List<? extends Number> numbers) {
double sum = 0.0;
for (Number n : numbers) {
sum += n.doubleValue();
} return sum;
} public static void main(String[] args) {
List<Integer> ints = List.of(10, 20, 30);
List<Double> doubles = List.of(1.5, 2.5, 3.5);
System.out.println("Ints Sum: " + sumOfList(ints));
System.out.println("Doubles Sum: " + sumOfList(doubles));
}
}