Recommended resources
The freely available resources this lesson will direct you to.
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.
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.
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.

Take a moment to analyze the code:
- There's a class hierarchy:
C3extendsC2, which extendsC1. processAll1takes aCollection<T>and aMyConsumer<T>.processAll2takes aCollection<T>and aMyConsumer<? 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 ofList<Object>). - Upper-Bounded Wildcards (
? extends T): Used for "producer" structures where you primarily read values. It allows you to pass a collection ofTor 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 ofTor any of its supertypes. You can safely add elements of typeT, but reading elements yields onlyObject.
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.