Blog

  • 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.

Tags: Java