Back to Blog
Java

Java extends vs super Generics: When to Use Each

java extends vs super generics: Understand the difference between extends and super wildcards in Java generics, when to use each, and how PECS guides your design.

Java genericswildcardsPECStype safetyJava collections
Diagram illustrating Java generics wildcards extends and super with producer and consumer roles.

java extends vs super generics requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you work with Java generics, ? extends T and ? super T appear in method signatures and variable declarations. They both introduce bounded wildcards, but they serve opposite purposes. ? extends T allows a type that is a subtype of T, while ? super T allows a type that is a supertype of T. The choice affects whether you can read from or write to a generic structure.

The Core Difference Between extends and super

In Java, ? extends Number means the actual type argument can be Number or any subclass such as Integer, Double, or BigDecimal. When you use this wildcard, you can safely read values as Number, but you cannot add elements to the collection because the compiler cannot guarantee that the added element is compatible with the unknown concrete type.

? super Integer means the actual type argument can be Integer or any supertype such as Number, Object, or Comparable. With this wildcard, you can safely add an Integer because any supertype can hold an Integer, but you cannot read a specific type because the actual type could be Object or Number.

This asymmetry is the foundation of the PECS rule: Producer Extends, Consumer Super.

Producer Extends, Consumer Super (PECS)

The PECS principle comes from Joshua Bloch's Effective Java. It states that if a method reads elements from a collection, use ? extends T; if it writes elements to a collection, use ? super T. If it does both, use the exact type without a wildcard.

Consider a method that copies elements from one list to another:

public static <T> void copy(List<? extends T> source, List<? super T> target) { for (T item : source) { target.add(item); } }

Here, source is a producer because it yields T values, so ? extends T is appropriate. target is a consumer because it accepts T values, so ? super T is appropriate. This signature allows copying from a List<Integer> into a List<Number> without losing type safety.

Practical Example: Reading with extends

When you only need to read from a collection, ? extends T gives you the flexibility to pass a collection of any subtype. For example, a method that sums a list of numbers:

public static double sum(List<? extends Number> numbers) { double total = 0.0; for (Number n : numbers) { total += n.doubleValue(); } return total; }

You can call this with List<Integer>, List<Double>, or List<Number>. Inside the method, each element is treated as Number, so you can call doubleValue() without casting. Attempting to add an element to numbers would fail to compile because the concrete type is unknown.

Practical Example: Writing with super

When you need to add elements to a collection, ? super T allows the collection to be of a supertype. For instance, a method that adds integers to any collection that can hold them:

public static void addIntegers(List<? super Integer> list) { list.add(5); list.add(10); }

This method accepts List<Integer>, List<Number>, or List<Object>. You can safely add Integer values because any supertype of Integer can store an Integer. Reading from this list is less useful because you can only get Object references, which may require casting.

When to Use extends vs super

Use ? extends T when the collection is a source of data that your method will read. This is common in methods that process a collection of a specific type or a subtype, such as calculating totals, filtering, or mapping.

Use ? super T when the collection is a destination for data that your method will write. This appears in methods that populate collections, like copying from a source or filling a list with default values.

If your method both reads and writes, avoid wildcards and use the exact type parameter, as in List<T>. This gives you full flexibility but restricts the caller to a list of exactly T, not a subtype or supertype.

Common Pitfalls with Wildcards

One frequent mistake is using ? extends T when you need to add elements. The compiler rejects the addition because the actual type could be a more specific subtype. For example:

List<? extends Number> numbers = new ArrayList<Integer>(); numbers.add(3.5); // Compilation error

Even though Double is a Number, the compiler cannot guarantee that the list is actually a List<Double>; it might be a List<Integer>.

Another pitfall is using ? super T when you need to read specific values. Reading from a List<? super Integer> yields Object, not Integer, so you lose type information.

Runtime Behavior and Type Safety

Wildcards do not change the runtime behavior of collections. They are purely compile-time constructs that enforce type safety. The Java compiler erases type arguments, so List<? extends Number> and List<Number> produce the same bytecode. The wildcard only restricts what the compiler allows you to do with the reference.

This means there is no performance overhead from using wildcards. The cost is only in your code's readability and maintainability. Using wildcards correctly prevents ClassCastException at runtime by moving type checks to compile time.

Compatibility and Maintainability Considerations

Wildcards improve API flexibility. A method that accepts List<? extends Number> can be called with List<Integer> without forcing the caller to change data types. This is essential when designing libraries that need to work with a variety of types.

However, overusing wildcards can make signatures harder to read. If a method's wildcard is not clearly a producer or consumer, developers may confuse its intent. Document the role of each wildcard parameter, and rely on the PECS rule to keep signatures intuitive.

When you update a method that uses wildcards, changing from ? extends T to ? super T can break callers. For example, a method that previously accepted List<? extends Number> now requires a list that can hold Number or a supertype, which may reject List<Integer> calls. Keep the wildcard direction stable unless the method's behavior genuinely changes.

Final Technical Note: Wildcards in Method Return Types

Wildcards can also appear in return types, but this is rarely useful. Returning List<? extends Number> tells the caller that the list contains some subtype of Number, but the caller cannot add elements to it. In most cases, it is better to return a concrete type like List<Number> or use a type parameter T to preserve type information. If you need to hide the concrete type, a wildcard return can prevent the caller from modifying the collection, but it also limits what the caller can do with it.

java extends vs super generics: Practical Usage and Code Exa | RYUSLOG DEV