Skip to Content
Course content

63: Wildcards in Generics

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a junior dev who was building a financial reporting module. He had written a perfectly logical method to sum up a list of values: public double sum(List<Number> numbers). Everything looked great until he tried to pass a List<Integer> into it. He was genuinely frustrated—he knew Integer was a subclass of Number, so why on earth was the Java compiler telling him the types were incompatible? He felt like he was fighting the language rather than using it.

This is the "Generics Trap." In Java, generics are invariant. This means that while an Integer is a Number, a List<Integer> is not a List<Number>. If Java allowed this, you could accidentally add a Double into a list that was supposed to only hold Integers, which would crash your program with a ClassCastException at runtime. To solve this without losing type safety, we use wildcards.

Expanding Read Access with Upper Bounds

When you want a method to accept a list of Numbers, or any subclass of Number (like Integer, Double, or Long), you use the ? extends T syntax. This is known as an upper bound.

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

By using ? extends Number, you're telling the compiler: "I don't care exactly what specific subtype of Number this list holds, as long as it's some kind of Number." Now, our friend from the anecdote can pass in a List<Integer> or a List<Double> without a hitch.

There is a catch, though. When you use extends, the list becomes effectively read-only for that specific type. You can pull Number objects out of the list, but you cannot put anything into it (except for null). Why? Because the compiler doesn't know if the list is actually a List<Integer> or a List<Double>, so it forbids adding elements to prevent you from putting a Double into an Integer list.

Allowing Writes with Lower Bounds

On the flip side, you'll run into situations where you want to add elements to a collection. If you're writing a utility that moves items from one list to another, you need a lower bound: ? super T.

Imagine you have a method that adds a list of integers to a destination collection. You don't just want it to work for a List<Integer>; it should also work for a List<Number> or even a List<Object>, because all of those can safely hold an integer.

public void addIntegers(List<? super Integer> destination, List<Integer> source) {
    for (Integer i : source) {
        destination.add(i);
    }
}

The ? super Integer wildcard tells Java: "This list can hold Integers, or any supertype of Integer." This is the opposite of the upper bound. Here, you can safely write to the list, but you can't safely read from it as anything more specific than an Object, because the list might be a List<Object>.

I usually tell my mentees to remember the PECS mnemonic: Producer Extends, Consumer Super. If your collection is producing values for you to use, use extends. If your collection is consuming values that you are putting into it, use super.




📋 Practical Task

Exercise: Implementing a Flexible Asset Transfer System

You are building a system for a portfolio manager. You have a class hierarchy where Asset is the base class, and Stock and Bond are subclasses of Asset.

Your task is to implement a PortfolioManager class with two methods:

  1. public double calculateTotalValue(List<? extends Asset> assets): This method should iterate through a list of assets and sum their values (assume the Asset class has a getValue() method). It must be able to accept a List<Stock>, a List<Bond>, or a List<Asset>.
  2. public void transferAssets(List<? extends Asset> source, List<? super Asset> destination): This method should move all assets from the source list into the destination list.

Requirements:

  • Ensure the calculateTotalValue method uses an upper-bound wildcard to allow any asset subtype.
  • Ensure the transferAssets method uses both an upper-bound for the source (the producer) and a lower-bound for the destination (the consumer).
  • Verify your logic by creating a List<Stock> and transferring its contents into a List<Asset>.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.