Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Generics with Wildcards: Flexible API Design

Hello! Welcome to the first lesson in our course on advanced Java. As a full-stack developer with a solid foundation in Java, you're already familiar with the power of Object-Oriented Programming and the Java Collections Framework. This course is designed to build on that experience, exploring the more intricate and powerful features of the language that enable you to write cleaner, more efficient, and more robust code.

This first module is all about mastering Advanced Generics and Type Safety. Today, we'll tackle the first learning outcome: applying upper-bounded (? extends T) and lower-bounded (? super T) wildcards to create flexible APIs.

We'll start by revisiting a core concept in Java's type system—invariance—to understand why wildcards are necessary. Then, we'll dive into how to use both upper and lower-bounded wildcards to make your methods that work with generic collections more flexible and powerful.

Let's begin.

The Problem: Why List<Integer> is Not a List<Number>

You know from your experience with OOP that if Integer is a subtype of Number, you can assign an Integer instance to a Number variable. This is called polymorphism.

Number myNumber = 10; // An Integer is-a Number

However, this principle does not apply directly to generic types. A List<Integer> is not considered a subtype of List<Number>. This property is called invariance. While it might seem counterintuitive, this rule exists to enforce type safety at compile time, preventing runtime errors.

Let's watch a short video that explains this problem clearly by contrasting the behavior of arrays (which are covariant) with collections (which are invariant).

Generics and Wildcards in Java | Part 2 | Invariance vs Covariance vs Contravariance | Geekific

This video from Geekific explains why the invariance of generic collections is a crucial feature for type safety, and sets the stage for why we need wildcards.

Watch the section from the beginning to 1:38. Pay close attention to the ArrayStoreException example and how invariance in generics moves this type of error from runtime to compile time.

As the video explains, if List<Integer> were treated as a List<Number>, nothing would stop you from adding a Double to it, leading to a "heap pollution" scenario where the list contains an unexpected type.

List<Integer> intList = new ArrayList<>();
// List<Number> numList = intList; // This is a compile-time error!
// If it were allowed, this next line would be possible:
// numList.add(3.14); // Corrupts intList with a Double!

// The compiler prevents this to guarantee type safety.
Integer myInt = intList.get(0); // This should always be safe.

This compile-time safety is a huge benefit, but it comes at the cost of flexibility. How do we write a single method that can, for example, calculate the sum of numbers in a List<Integer>, a List<Double>, or a List<Number> without writing three separate methods?

This is where wildcards come in.

Upper-Bounded Wildcards: Reading from a Flexible Source (? extends T)

An upper-bounded wildcard, written as ? extends Type, allows you to relax the strict invariance rule. It tells the compiler that you accept a collection of Type or any of its subtypes.

For our sum method example, we can use List<? extends Number>. This makes the method compatible with List<Integer>, List<Double>, and List<Number>, because Integer and Double both extend Number.

This type of collection can be thought of as a producer—you can safely get or read values from it and know that they will be, at minimum, of the bounded type (Number in this case).

Let's watch a demonstration.

Generics and Wildcards in Java | Part 2 | Invariance vs Covariance vs Contravariance | Geekific

This segment of the Geekific video demonstrates the practical use of an upper-bounded wildcard ? extends Number for a method that reads from a collection.

Watch from 2:51 to 4:12. Note how the method signature is changed to use the wildcard and how it restricts you from adding new elements to the list.

The key restriction with ? extends T is that you cannot add elements to the collection (except null). The compiler prohibits this because it cannot confirm the exact type of the list. If your method receives a List<? extends Number>, it could be a List<Integer> or a List<Double>. The compiler cannot allow you to add an Integer to what might be a List<Double>, so it forbids all additions to guarantee type safety.

For a more detailed textual explanation and another code example, please read the following section from the official Java documentation.

Wildcards - Dev.java

This official Java tutorial from Dev.java provides a concise explanation of upper-bounded wildcards with a clear sumOfList code example.

Read the section titled 'Upper Bounded Wildcards'. It reinforces what you saw in the video and provides another clear code example.

Lower-Bounded Wildcards: Writing to a Flexible Target (? super T)

Now, let's consider the opposite scenario. What if you want to write a method that adds integers to a list, and you want that method to work with a List<Integer>, a List<Number>, or even a List<Object>?

This is where a lower-bounded wildcard, ? super Type, is used. It means you accept a collection of Type or any of its supertypes. For our example, List<? super Integer> would accept a List<Integer>, List<Number>, or List<Object>, because Number and Object are both supertypes of Integer.

You can think of this as a consumer—you can safely put or write values of type Type into it.

Generics and Wildcards in Java | Part 2 | Invariance vs Covariance vs Contravariance | Geekific

The next segment of the Geekific video shows how to use a lower-bounded wildcard ? super Integer to create a method that writes to a collection.

Watch from 4:12 to 5:31. Notice how this wildcard allows you to pass lists of supertypes and what happens when you try to retrieve elements from such a list.

The trade-off here is with reading. If you retrieve an element from a List<? super Integer>, the only thing the compiler can guarantee is that it's an Object. It could be an Integer, a Number, or any other Object if the passed list was List<Object>. Therefore, unless you are retrieving an Object, reading from a lower-bounded collection isn't type-safe.

The Dev.java tutorial also has a great section on this.

Wildcards - Dev.java

Let's read the 'Lower Bounded Wildcards' section from the Dev.java tutorial. It provides a clear addNumbers example that complements the video.

Read the section titled 'Lower Bounded Wildcards'. Focus on how List<? super Integer> allows the addNumbers method to be more flexible.

A Practical Code Example

Let's look at a concrete code example that demonstrates how using a wildcard can make an API more flexible. The image below shows two methods, processAll1 and processAll2.

Java Code Example: Upper and Lower Bounded Wildcards
This code defines a custom `MyConsumer` interface and two methods. `processAll1` uses a rigid type signature, while `processAll2` uses `? super T` on the consumer to make the method more flexible and reusable.

Take a moment to analyze the code:

  • There's a class hierarchy: C3 extends C2, which extends C1.
  • processAll1 takes a Collection<T> and a MyConsumer<T>.
  • processAll2 takes a Collection<T> and a MyConsumer<? super T>.

In the main method, a call to processAll1(c3Collection, c1Consumer) would result in a compile-time error. This is because c1Consumer is of type MyConsumer<C1>, which is not compatible with the expected MyConsumer<C3>.

However, the call processAll2(c3Collection, c1Consumer) compiles successfully. Why? Because processAll2 declares its consumer as MyConsumer<? super C3>, which means it can accept a consumer of C3 or any of its supertypes (C2 or C1). Since C1 is a supertype of C3, the c1Consumer is a valid argument.

This is a perfect illustration of how ? super T makes an API more flexible for a "consumer" parameter.

Conclusion

In this lesson, we've explored how to overcome the limitations of Java's generic invariance using bounded wildcards. This is fundamental to writing flexible and reusable libraries and APIs.

Here are the key takeaways:

  • Invariance: Generic types are invariant by default to ensure compile-time type safety (e.g., List<String> is not a subtype of List<Object>).
  • Upper-Bounded Wildcards (? extends T): Used for "producer" structures where you primarily read values. It allows you to pass a collection of T or any of its subtypes. You cannot safely add elements to such a collection.
  • Lower-Bounded Wildcards (? super T): Used for "consumer" structures where you primarily write values. It allows you to pass a collection of T or any of its supertypes. You can safely add elements of type T, but reading elements yields only Object.

You might have noticed a pattern: we use extends when we get values out of a structure, and super when we put values in.

In our next lesson, we will formalize this concept by learning about the PECS principle (Producer-Extends, Consumer-Super). This simple mnemonic will serve as a powerful guideline to help you decide exactly which wildcard to use in any given situation.

Can't find a good explanation? Sign up and we'll make it for you

Sign up