A Guide to Generic Type Arguments in Java

Posted By Amit Kumar | 30-Nov-2018

Generics capabilities were added in Java way back in version J2SE 1.5, but in Java 8, suddenly the Javadocs are filled with method signatures like the following :

 

static <K extends Comparable<? super K>,V> Comparator<Map.Entry<K,V>>
    comparingByKey()

//java.util.Comparator
static <T,U extends Comparable<? super U>> Comparator<T> comparing(
    Function<? super T,? extends U> keyExtractor)

//java.util.stream.Collectors
static <T,K,D,A,M extends Map<K, D>> Collector<T,?,M> groupingBy(
    Function<? super T,? extends K> classifier, Supplier<M> mapFactory,
    Collector<? super T,A,D> downstream)

In this blog, we look to understand the above parameters being used

One of the simplest usages of Generic Type parameters we know is

List<String> strings = new ArrayList<String>();
//and since Java 7 we can also use 
List<String> strings = new ArrayList<>();

Declaring the data type of the collection accomplishes two goals:
We can’t place the wrong type inside a collection and an explicit cast is not required to retrieve the values. Example - 1

 

List<String> strings = new ArrayList<>();
strings.add("Hello");
strings.add("World");
// strings.add(new Date());    
// Integer i = strings.get(0); 

for (String s : strings) {     
    System.out.printf("%s has length %d%n", s, s.length());
}

//Let us look through the List interface (public interface List<E> extends Collection<E>) with E as the type parameter

boolean	add(E e)                           
boolean	addAll(Collection<? extends E> c)  
void	clear()                            
boolean	contains(Object o)                 
boolean	containsAll(Collection<?> c)       
E	get(int index) 

 

Some of these methods use the declared generic type, E, as either an argument or a return type. Some (specifically, clear and contains) don’t use the type at all. Others involve some kind of a wildcard, using a question mark.

One of the most common mistakes a person can make in using the above functions is - getting confused between the relationship in List<tring> and List<Object>. Consider the add method in the List<E> interface - 

 

//declare & initialise , List<Object>
List<Object> objects = new ArrayList<Object>();
objects.add("Hello World");
objects.add(new Date());

List<String> strings = new ArrayList<>();
String s = "Hello World";
Object o = s;                          
strings.add(o)      //does not compile

 

The problem is that List<String> is not a subclass of List<Object>. The only instances we can add to it are of the type String and hence we say that the parameterized type is invariant.
If we could assign a List<String> to a List<Object> we could add something that wasn’t string to the list, causing a cast exception when you try to retrieve it using the List<String> reference.

For similar situations do not arise we can use wildcards instead of invariant parameter types

Wildcards
A wildcard is a type of argument that uses a question mark, ? with an optional upper or lower bound.

Upper Bounded Wildcards
An upper bounded wildcard defines the super class limit. For example define a list of numbers that can belongs, doubles, and even BigDecimal instances -

 

private static double sumList(List<? extends Number> list) {
    return list.stream()
               .mapToDouble(Number::doubleValue)
               .sum();
}

public static void main(String[] args) {
    List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
    List<Double> doubles = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
    List<BigDecimal> bigDecimals = Arrays.asList(
        new BigDecimal("1.0"),
        new BigDecimal("2.0"),
        new BigDecimal("3.0"),
        new BigDecimal("4.0"),
        new BigDecimal("5.0")
    );

    System.out.printf("ints sum is         %s%n", sumList(ints));
    System.out.printf("doubles sum is      %s%n", sumList(doubles));
    System.out.printf("big decimals sum is %s%n", sumList(bigDecimals));
}

 

Lower Bounded Wildcards
A lower bounded wildcard means any parent of the class is acceptable. We use super keyword to specify a lower bound. The implication, in the case of a List<? super Number>

Consider a method called numsUpTo that takes two arguments, an integer, and a list to populate with all the numbers up to the first argument, as in Example.

 

public void numsUpTo(Integer num, List<? super Integer> output) {
    IntStream.rangeClosed(1, num)
             .forEach(output::add);
}

ArrayList<Integer> integerList = new ArrayList<>();
ArrayList<Number>  numberList = new ArrayList<>();
ArrayList<Object>  objectList = new ArrayList<>();
numsUpTo(5, integerList);
numsUpTo(5, numberList);
numsUpTo(5, objectList);

 

Multiple Bounds
One final note before looking at examples from the Java 8 API. A type parameter can have multiple bounds. The bounds are separated by an ampersand when they are defined:

T extends Runnable & AutoCloseable
You can have as many interface bounds as you like, but only one can be a class. If you have a class as a bound, it must be first in the list.

Examples from the Java 8 API
With all that in mind, now it’s time to review some examples from the Java 8 docs.

Stream.max
In the java.util.stream.Stream interface, consider the max method:

Optional<T> max(Comparator<? super T> comparator)
Note the use of the lower bounded wildcard in the Comparator. The max method returns the maximum element of a stream by applying the supplied Comparator to it. The return type is Optional<T>, because there may not be a return value if the stream is empty. The method wraps the maximum in an Optional if there is one, and returns an empty Optional if not.

Request for Proposal

Recaptcha is required.

Sending message..