The previous tutorial covered wildcard bounds, producers, consumers and how they work with get and put methods; this tutorial explorers wildcard bounds in Java API.

Following class hierarchy is used in explanation:

Java Generics Bounded Wildcard

Wildcard bounds in Java API

Java API extensively uses wildcard bounds, and we should learn to tackle them. We start with couple of easy ones from java.util.Collections and then move on to a difficult ones from java.util.Stream.

Take the Collections class method <T> void fill(List<? super T> list, T obj). If wildcard bound is not there and T is Dog then we can pass List<Dog and nothing else and fill it with Dog. But with List<? super T>, we can fill a Dog not only to List<Dog> but also to List<Pet>. The List<? super T> acts as consumer, which allows invocation of put methods as explained in previous tutorial.


List<Pet> pets = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();

Collections.fill(dogs, new Dog());
Collections.fill(pets, new Dog());
 
 

Next, look at the public static <T> void copy​(List<? super T> dest, List<? extends T> src) method of Collections class. If wildcards are not used then we can copy list of a type to another list of same type.


List<Pet> pets = new ArrayList<>();
List<Pet> allPets = new ArrayList<>();

Collections.copy(allPets, pets);

However, with wildcards bounds; if T is Dog, then we can copy list of Dog or Bulldog to list of Dog or Pet. The List<? extends T> src acts as producer that allows only the get methods as explained in previous tutorial, while List<? super T> dst acts as consumer which allows put methods.


List<Pet> pets = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
List<Bulldog> bulldogs = new ArrayList<>();
List<Cat> cats = new ArrayList<>();

Collections.copy(pets, pets);
Collections.copy(pets, dogs);
Collections.copy(pets, bulldogs);
Collections.copy(pets, cats);

Collections.copy(dogs, dogs);
Collections.copy(dogs, bulldogs);

// Error: can't copy String list to Pet list
List<String> strings = new ArrayList<>();
Collections.copy(pets, strings);   // error

The final example is quizzical java.util.Stream.map() method which is normally used with lambda expression as follows:

  
List<String> list = Stream.of("USA", "China", "India")
                        .map(s -> s.toLowerCase())
                        .collect(Collectors.toList());
  

At first glance, the map() method signature is quite cryptic.


public <R> Stream<R> map(Function<? super T, ? extends R> mapper)

It broadly means:

  • map input parameter is a Function that accepts input of type T and returns an object of type R.
  • map returns a Stream of objects of type R.
  • R is the type parameter defined at method level, so function can return instance of any type.

But, what does Function<? super T, ? extends R> really signifies? To know that, we need to understand the implications had it been designed without any wildcards as public <R> Stream<R> map(Function<T, R> mapper)

List<Dog> dogs = new ArrayList<>();    

Function<Dog, Cat> fn = e -> new Cat();

dogs.stream().map(fn);
  

The above code, defines a Function that takes Dog as input and returns a new Cat and use it to map a stream of Dog List<Dog>. If map api is without any wildcards then it accepts only the Function with input as Dog and nothing else. If we try to pass Function<Pet, Cat> or any other input type results in compiler error. Return type R can be any type.

To be more flexible, API uses wildcard bounds. For the function input, there are two options: Function<? extends T, R> or Function<? super T, R>. The first option ? extents T is more flexible, but it makes the input as a Producer. To execute the lambda expression, map() calls the functional method Function.apply(T t), which is a put method as it parameter T is a type parameter. As we have seen in the previous tutorial, put methods are not allowed on Producers. So, the extends is ruled out.

 
 

The other option, ? super T acts as Consumer, and there is no bar on calling apply(T t) on it. It allows T and all its super type as input. Now, we are free to use not only Dog but also its super class, Pet, as input. Following combinations are valid:


List<Pet> pets = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
List<Bulldog> bulldogs = new ArrayList<>();

Function<Pet, String> petfn = e -> "foo";
Function<Dog, String> dogfn = e -> "bar";
Function<Bulldog, String> bulldogfn = e -> "zoo";

// for List<Pet> only the functions whose input is Pet or its super are allowed
pets.stream().map(petfn);     
pets.stream().map(dogfn);       // error - Dog is not super of Pet 
pets.stream().map(bulldogfn);   // error - Bulldog is not super of Pet

// for List<Dog> only the functions whose input is Dog or its super are allowed
dogs.stream().map(petfn);     
dogs.stream().map(dogfn);
dogs.stream().map(bulldogfn);   // error - Bulldog is not super of Dog 
        
// for List<Bulldog> only the functions whose input is Bulldog or its super are allowed        
bulldogs.stream().map(petfn);
bulldogs.stream().map(dogfn);
bulldogs.stream().map(bulldogfn);

/*
* Function<Pet, String> can map list of Pet, Dog and Bulldog 
*   - function input is Pet and it is super of Pet, Dog and Bulldog
* Function<Dog, String> can map list of Dog and Bulldog
*   - function input is Dog and it is super of Dog and Bulldog
* Function<Bulldog, String> can map list of Bulldog
*   - function input is Bulldog and it is super Bulldog
*/

Next, let’s tackle the function return type if function is specified as Function<? super T, R>

  
Function<Pet, Pet> petfn = e -> new Bulldog();
Function<Pet, Dog> dogfn = e -> new Bulldog();
Function<Pet, Bulldog> bulldogfn = e -> new Bulldog();

Stream<Pet> s1 = pets.stream.map(petfn);   // allowed: only petfn
Stream<Dog> s2 = pets.stream.map(dogfn);   // allowed: only dogfn
Stream<Bulldog> s3 = pets.stream.map(bulldogfn);   // allowed: only bulldogfn
  

When map is assigned to Stream<Pet>, R is set to Pet; only petfn is allowed and we can neither use dogfn nor bulldogfn. Similarly, for Stream<Dog> we can use a Function with return type Dog and nothing else.

To make things more flexible, API uses wildcard bounds. For return type, again there are two options: ? extends R and ? super R. As producer and consumer is not applicable for return type, the most flexible type, ? extends R, is chosen.


Function<Pet, Pet> petfn = e -> new Cat();
Function<Pet, Dog> dogfn = e -> new Dog();
Function<Pet, Bulldog> bulldogfn = e -> new Bulldog();

Stream<Pet> s1 = pets.stream().map(bulldogfn); // allowed: petfn, dogfn and bulldogfn
Stream<Dog> s2 = pets.stream().map(bulldogfn); // allowed: dogfn and bulldogfn
Stream<Bulldog> s3 = pets.stream().map(bulldogfn); // allowed: bulldogfn

In the first map statement, R is Pet so then functions - petfn, dogfn and bulldogfn - that returns Pet or its subtypes are allowed. In the second map call, R is Dog and functions - dogfn and bulldogfn - that returns Dog or Bulldog are allowed.

Just like wildcard bounds, we can bind the type parameter itself and the next tutorial explains it.