Kohei Nozaki's blog 

Entries tagged [generics]

Java Generics wildcards


Posted on Friday Jul 17, 2020 at 06:49PM in Technology


This entry is a quick introduction to Java Generics wildcards, which make Java code more flexible and type-safe.

Wildcards with extends

Wildcards with extends provide flexibility to code which gets a value out of a container object. For example, let’s consider the following variable definition:

List<? extends Number> list_extendsNumber;

You can think of it as a List which contains elements whose type is a subtype of Number. For example, we can assign the following instances to the variable:

list_extendsNumber = new ArrayList<Number>();
list_extendsNumber = new ArrayList<Integer>(); // Integer extends Number
list_extendsNumber = new ArrayList<BigDecimal>(); // BigDecimal extends Number

But the following doesn’t compile since neither of them is a subtype of Number:

list_extendsNumber = new ArrayList<String>(); // compilation error; a String is not a Number
list_extendsNumber = new ArrayList<Object>(); // compilation error; Number extends Object but not the other way around

As you can see from the examples above, it’s guaranteed that a List which is assigned to the list_extendsNumber variable contains a Number or a value whose type is a subtype of Number. We can use the variable for getting a value out of it as a Number. For example:

// It's guaranteed that it contains a Number or its subtype
Number firstNumber = list_extendsNumber.get(0);

You can even write some utility method like this:

double avg(List<? extends Number> nums) {
    return nums.stream().mapToDouble(Number::doubleValue).average().orElse(0);
}

The avg() method accepts any List whose type parameter is a subtype of Number. For example, the following code which calls the method works fine:

List<Number> nums = Arrays.asList(1, 0.5d, 3);
assert avg(nums) == 1.5d;

List<Integer> ints = Arrays.asList(1, 2, 3);
assert avg(ints) == 2d;

List<BigDecimal> bds = Arrays.asList(new BigDecimal(1), new BigDecimal(2), new BigDecimal(3));
assert avg(bds) == 2d;

But the following is invalid:

List<String> strs = Arrays.asList("foo", "bar", "baz");
avg(strs) // compilation error; a String is not a Number

List<Object> objs = Arrays.asList("foo", 0.5d, 3);
avg(objs) // compilation error; Number extends Object but not the other way around

What’s good about it? Let’s see what happens if we don’t use the wildcard here. Now the method looks like this:

double avg(List<Number> nums) {
    return nums.stream().mapToDouble(Number::doubleValue).average().orElse(0);
}

Now the method can accept only List<Number> that is much less flexible. The following still works:

List<Number> nums = Arrays.asList(1, 0.5d, 3);
assert avg(nums) == 1.5d; // still fine

But neither of them compiles anymore:

List<Integer> ints = Arrays.asList(1, 2, 3);
assert avg(ints) == 2d; // compilation error

List<BigDecimal> bds = Arrays.asList(new BigDecimal(1), new BigDecimal(2), new BigDecimal(3));
assert avg(bds) == 2d; // compilation error

Which means that when you write code which gets values out of a container object (e.g. List), using wildcards with extends provides more flexibility. In other words, your code (or method) will be able to accept a wider range of parameters if wildcards with extends are used appropriately.

The limitation imposed by the use of wildcards with extends is that you won’t be able to put any value except for null through a variable which uses extends. For example:

List<? extends Number> list_extendsNumber = new ArrayList<Integer>();
list_extendsNumber.add(null); // compiles; null is the only exception
list_extendsNumber.add(1); // compilation error

Why? Remember that we can assign any List whose type parameter is a subtype of Number. For example, you can also assign a List whose type parameter is BigDecimal to the list_extendsNumber variable. In that case adding an Integer to the List should be invalid since an Integer is not a BigDecimal. The compiler prevents it from happening thanks to generics and wildcards. Adding a null is fine since null is not tied to a particular type.

Wildcards with super

While wildcards with extends make code which gets a value more flexible, wildcards with super provide flexibility to code which puts a value into a container object. Let’s consider the following variable definition:

List<? super Integer> list_superInteger;

It means a List which contains elements whose type is a supertype of Integer. For example, we can assign the following instances into the variable:

list_superInteger = new ArrayList<Integer>();
list_superInteger = new ArrayList<Number>(); // Number is a supertype of Integer
list_superInteger = new ArrayList<Object>(); // Object is a supertype of Integer

But the following doesn’t compile:

list_superInteger = new ArrayList<String>(); // compilation error; String is not a supertype of Integer

What we can see from the example above is that it’s guaranteed that the List which is assigned to the list_superInteger variable can accept an Integer. We can use it for putting a value into it. For example:

// It's guaranteed that it can accept an Integer
list_superInteger.put(123);

Or you can write some method with it like this:

void addInts(List<? super Integer> ints) {
    Collections.addAll(ints, 1, 2, 3);
}

The method can accept any of the following:

List<Integer> ints = new ArrayList<>();
addInts(ints);
assert ints.toString().equals("[1, 2, 3]");

List<Number> nums = new ArrayList<>();
addInts(nums);
assert nums.toString().equals("[1, 2, 3]");

List<Object> objs = new ArrayList<>();
addInts(objs);
assert objs.toString().equals("[1, 2, 3]");

But the following doesn’t compile:

List<String> strs = new ArrayList<>();
addInts(strs); // compilation error; List<String> cannot accept an Integer

If we didn’t use the wildcard, the method would be less flexible. Let’s say now we have this without a wildcard:

void addInts(List<Integer> ints) {
    Collections.addAll(ints, 1, 2, 3);
}

The following still compiles:

List<Integer> ints = new ArrayList<>();
addInts(ints);
assert ints.toString().equals("[1, 2, 3]");

But the following does not compile anymore:

List<Number> nums = new ArrayList<>();
addInts(nums); // compilation error

List<Object> objs = new ArrayList<>();
addInts(objs); // compilation error

The limitation imposed by the use of wildcards with super is that you will be able to get a value out of a container object only with the Object type. In other words, if you want to get a value out of the list_superInteger variable, this is the only thing you can do:

List<Integer> ints = new ArrayList<>();
ints.add(123);
List<? super Integer> list_superInteger = ints;

Object head = list_superInteger.get(0); // only Object can be used as the type of head
assert head.equals(123);

The following doesn’t compile:

Integer head = list_superInteger.get(0); // compilation error

Why? Remember that we can also assign a List<Number> or a List<Object> to the list_superInteger variable, which means that the only type that we can safely use to get a value out of it is the Object type.

The Get and Put Principle

As we have seen, appropriate use of wildcards provides more flexibility to your code. To summarize when we should use which, there is a good principle to follow:

“The Get and Put Principle: Use an extends wildcard when you only get values out of a structure, use a super wildcard when you only put values into a structure, and don’t use a wildcard when you both get and put.” - Naftalin, M., Wadler, P. (2007). Java Generics And Collections, O’Reilly. p.19

Functional interfaces exemplify this principle. For example, consider a method which accepts objects that implement functional interfaces as follows:

void myMethod(Supplier<? extends Number> numberSupplier, Consumer<? super String> stringConsumer) {
    Number number = numberSupplier.get();
    String result = "I got a number whose value in double is: " + number.doubleValue();
    stringConsumer.accept(result);
}

Supplier is something which you can apply the get principle to; you only get values out of it. And Consumer is the same for the put principle; you only put values into it.

myMethod() can accept various types of parameters. The user of the method doesn’t necessarily have to pass a Supplier<Number> and a Consumer<String>. The user can also pass a Supplier<BigDecimal> and a Consumer<Object> as follows, but that’s possible only with the use of the wildcards:

Supplier<BigDecimal> bigDecimalSupplier = () -> new BigDecimal("0.5");
AtomicReference<Object> reference = new AtomicReference<>();
Consumer<Object> objectConsumer = reference::set;

myMethod(bigDecimalSupplier, objectConsumer);

assert reference.get().equals("I got a number whose value in double is: 0.5");

Conclusion

We have seen how we can use wildcards, what the benefits of them are and when to use them. To make your code more flexible and reusable, especially when you write a method or a constructor, it’s good practice to think if you can apply wildcards to code which handles an object that has a type parameter. Remembering the get and put principle will be helpful when you do so.