Monday, May 2, 2022

Code: Java Generics: Bounded Wildcards

Java wildcards are the kind of fun that make you want to bathe with your toaster. But they're not so bad if you can remember the simple rules, which come with practice. Say we have three generic lists, one of some random class, one of its superclass, and another of a subclass of the original class:

List<Number> ln;
List<Integer> li;
List<Object> lo;

Then, using wildcards, we can create some wildcard lists. One that uses "extends" of the original class, and the other that uses "super" of the original class.

List<? extends Number> numProducer;
List<? super Number> numConsumer;

Can you remember which of the top 3 lists can be assigned to these two wildcard lists? Don't forget PECS: Producer-extends, Consume-super.

// same type
numProducer = ln;
numConsumer = ln;

// subtype
numProducer = li;
// numConsumer = li; // doesn't compile: Integer isn't a superclass of Number

// supertype
// numProducer = lo; // doesn't compile: Object doesn't extend Number
numConsumer = lo;

Remember that ? extends Number and ? super Number can both be set to an object of type Number. If you remembered that, this was probably an easy exercise. The Java compiler prevents you from making class casting mistakes, so 
you can't set a super to a subclass and you can't set an extends to a superclass.

This might seem weird. If you can set numConsumer to a List<Number>, why can't you set it to List<Integer>? Every Integer is a Number, so any operation you could perform with an item from numConsumer should work. But don't think of numConsumer as a random collection of Numbers and other instances of Number's superclasses. Instead, remember the entire point of writing a lower-bounded wildcard is that the unknown type is indeed known at compile time and we want to restrict the type to only Number and Number's superclasses. Why might we want to do this? Abstraction.

The reason for restricting numProducer to subclasses of Number is even more obvious. If we set numProducer to a list of Objects, we might try to perform numeric operations on a list containing Strings, or Sets, or other non-Numbers.

You really shouldn't be setting any wildcard values directly anyway. They're usually used as method parameters and you simply get things out of (produce) or put things into (consume) your wildcard object. Like in:
public Number addThese(List<? extends Number> addends) {
Number sum = 0;
for (Number n : addends) {
sum = addTwoNumbers(sum, n);
}
return sum;
}
Notice that we're getting the value out of our producer. What type of objects can we get out of wildcard producers and consumers, and what type of objects can we put in to each?

Getting objects out:

Number num1 = numProducer.get(0);
// Number num2 = numConsumer.get(0); // doesn't compile; could return non-Number
// Integer int1 = numProducer.get(0); // doesn't compile; could return non-Integer
// Integer int2 = numConsumer.get(0); // doesn't compile; could return non-Integer
Object obj1 = numProducer.get(0);
Object obj2 = numConsumer.get(0); // Fine only because everything is an Object 

The last line only works because I couldn't think of 3 levels of classes where the parent class wasn't Object. If we used a different super class, an implicit cast from a consumer to its parent class wouldn't work:

// First create two wildcard Integer lists, then get an object from them
List<? extends Integer> intProducer = ...;
List<? super Integer> intConsumer = ...;
// Number numA = intConsumer.get(0); // doesn't compile; could return non-Number
Number numB = intProducer.get(0); // just to show this still works

This makes sense. We can only set value to its type or a subtype of its type. So only a producer can properly use getter methods like get() and only for its type or a superclass of its type. Consumers can't use getter methods (unless you implicitly cast to Object). So if producers can be informally thought of as read-only, consumers are write-only.

Putting objects in:

numConsumer.add(ln.get(0));
// numProducer.add(ln.get(0)); // doesn't compile; producer might be set to diff subtype
numConsumer.add(li.get(0));
// numProducer.add(li.get(0)); // doesn't compile; might be set to subtype
//numConsumer.add(lo.get(0)); // doesn't compile: might be set to subtype
// numProducer.add(lo.get(0)); // doesn't compile: Object doesn't extend Number

This time, it's only the consumer that can add any type of Number or Number subclasses. None of the producer calls work because whatever type the producer is set to, it could be incompatible with what it's getting.

Note that some of these examples would work if you added explicit casts.

Now, for reference, here's a cheat sheet with compilable code.