Appendix A. Generics and Java 8

Background

Generics capabilities were added in Java way back in version J2SE 1.5, but most Java developers only learned the minimum they needed to know about them to get the job done. With the advent of Java 8, suddenly the Javadocs are filled with method signatures like this one from java.util.Map.Entry:

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

or this one from java.util.Comparator:

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

or even this monster from 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)

Understanding the minimum isn’t going to be enough anymore. The purpose of this appendix is to help you break down similar signatures into understandable parts so that you can use the API productively.

What Everybody Knows

When you want to use a collection like List or Set, you declare the type of the contained elements by placing their class name in angle brackets:

List<String> strings = new ArrayList<String>();
Set<Employee> employees = new HashSet<Employee>();
Note

Java 7 made the syntax a bit easier by introducing the diamond operator used in the following code samples. Since the reference on the lefthand side declares the collection along with its contained type, like List<String> or List<Integer>, the instantiation on the same line doesn’t have to. You can simply write new ArrayList<>() without putting the type inside the angle brackets.

Declaring the data type of the collection accomplishes two goals:

  • You can’t accidentally place the wrong type inside a collection

  • You no longer need to cast a retrieved value to the proper type

For example, if you declare the strings variable as shown, then you can only add String instances to the collection and you automatically have a String when you retrieve an item, as in Example A-1.

Example A-1. Simple generics demo
List<String> strings = new ArrayList<>();
strings.add("Hello");
strings.add("World");
// strings.add(new Date());    1
// Integer i = strings.get(0); 1

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

Won’t compile

2

for-each loop knows the contained data type is String

Applying type safety to the insertion process is convenient, but developers rarely make that mistake. Being able to deal with the proper type on retrieval without having to cast first, however, saves a lot of code.1

The other thing all Java developers know is that you can’t add primitive types to generic collections. That means you can’t define List<int> or List<double>.2 Fortunately, the same version of Java that introduced generics also added auto-boxing and unboxing to the language. As a result, when you want to store primitives in a generic type, you declare the type using the wrapper class. See Example A-2.

Example A-2. Using primitives in generic collections
List<Integer> ints = new ArrayList<>();
ints.add(3); ints.add(1); ints.add(4);
ints.add(1); ints.add(9); ints.add(2);
System.out.println(ints);

for (int i : ints) {
    System.out.println(i);
}

Java takes care of wrapping the int values inside Integer instances on insertion, and extracts them from the Integer instances on retrieval. While there may be efficiency concerns regarding boxing and unboxing, the code is easy enough to write.

There’s one more aspect of generics that all Java developers know. When you’re reading the Javadocs about a class that uses generics, you use a capital letter in brackets for the type itself. For example, the docs for the List interface is:

public interface List<E> extends Collection<E>

Here, E is the type parameter. The methods in the interface use the same parameter. Example A-3 shows a sample of those methods.

Example A-3. Methods declared in the List interface
boolean	add(E e)                           1
boolean	addAll(Collection<? extends E> c)  2
void	clear()                            3
boolean	contains(Object o)                 3
boolean	containsAll(Collection<?> c)       4
E	get(int index)                      1
1

Using the type parameter E as an argument or return type

2

A bounded wildcard

3

Methods that do not involve the type itself

4

An unknown type

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 wildcard, using a question mark.

As a syntax note, it is legal to declare generic methods even in classes that aren’t generic. In that case, the generic parameter or parameters are declared as part of the method signatures. For example, here are some of the static methods from the utility class java.util.Collections:

static <T>   List<T>    emptyList()
static <K,V> Map<K,V>   emptyMap()
static <T>   boolean    addAll(Collection<? super T> c, T... elements)
static <T extends Object & Comparable<? super T>>
    T min(Collection<? extends T> coll)

Three of these methods declare a generic parameter called T. The emptyList method uses it to specify the contained type in the List. The emptyMap method uses K and V for key and value types in a generic map.

The addAll method declares the generic type T, but then uses a Collection<? super T> as the first argument of the method, as well as a variable argument list of type T. The syntax ? super T is a bounded wildcard, which will be the subject of the next section.

The min method shows how generic types may provide safety, but can make the documentation much harder to read. This signature is discussed in more detail in a later section, but for the record, T is bounded to be both a subclass of Object as well as implementing the Comparable interface, where Comparable is defined for T or any of its ancestors. The argument to the method involves any Collection of T or any of its descendants.

The wildcard is where we transition from syntax everyone knows to the parts with which you may not be comfortable. To prepare for that, consider the strange case when what looks like inheritance turns out not to be inheritance at all.

What Some Developers Don’t Realize

Many developers are surprised to learn that ArrayList<String> is not related in any practical way to ArrayList<Object>. You can add subclasses of Object to an Object collection, as in Example A-4.

Example A-4. Using a List<Object>
List<Object> objects = new ArrayList<Object>();
objects.add("Hello");
objects.add(LocalDate.now());
objects.add(3);
System.out.println(objects);

That much is OK. String is a subclass of Object, so you can assign a String to an Object reference. You would think that if you declared a list of strings, you could add objects to it. That, as they say, turns out not to be the case. See Example A-5.

Example A-5. Using a List<String> with objects
List<String> strings = new ArrayList<>();
String s = "abc";
Object o = s;                          1
// strings.add(o);                     2

// List<Object> moreObjects = strings; 3
// moreObjects.add(new Date());
// String s = moreObjects.get(0);      4
1

Allowed

2

Not allowed

3

Also not allowed, but pretend it was

4

Corrupted collection

Since String is a subclass of Object, you can assign a String reference to an Object reference. You can’t, however, add an Object reference to a List<String>, which feels strange. The problem is that List<String> is not a subclass of List<Object>. When declaring a type, the only instances you can add to it are of the declared type. That’s it. No sub- or superclass instances allowed. We say that the parameterized type is invariant.

The commented out section shows why List<String> is not a subclass of List<Object>. Say you could assign a List<String> to a List<Object>. Then, using the list of object references, you could add something that wasn’t a string to the list, which would cause a cast exception when you tried to retrieve it using the original reference to the list of strings. The compiler wouldn’t know any better.

Still, it seems reasonable that if you defined a list of numbers, you should be able to add integers, floats, and doubles to it. To accomplish that, we need type bounds that involve wildcards.

Wildcards and PECS

A wildcard is a type argument that uses a question mark, ?, which may or may not have an upper or lower bound.

Unbounded Wildcards

Type arguments without bounds are useful, but have limitations. If you declare a List of unbounded type, as in Example A-6, you can read from it but not write to it.

Example A-6. A List with an unbounded wildcard
List<?> stuff = new ArrayList<>();
// stuff.add("abc");            1
// stuff.add(new Object());
// stuff.add(3);
int numElements = stuff.size(); 2
1

No additions allowed

2

numElements is zero

That feels pretty useless, since there’s apparently no way to get anything into it. One use for them is that any method that takes a List<?> as an argument will accept any list at all when invoked (Example A-7).

Example A-7. Unbounded List as a method arg
private static void printList(List<?> list) {
    System.out.println(list);
}

public static void main(String[] args) {
    // ... create lists called ints, strings, stuff ...
    printList(ints);
    printList(strings);
    printList(stuff);
}

Recall the earlier example, which showed the containsAll method from List<E>:

boolean containsAll(Collection<?> c)

That method returns true only if all the elements of the collection appear in the current list. Since the argument uses an unbounded wildcard, the implementation is restricted to only:

  • Methods from Collection itself that don’t need the contained type, or

  • Methods from Object

In the case of containsAll, that’s perfectly acceptable. The default implementation (in AbstractCollection) in the reference implementation walks through the argument using iterator and invokes the contains method to check that each element in it is also inside the original list. Both iterator and contains are defined in Collection, and equals comes from Object, and the contains implementation delegates to the equals and hashCode methods of Object, which may have been overridden in the contained type. As far as the containsAll method is concerned, all the methods it needs are available. The restrictions on unbounded wildcards are not a problem.

The question mark forms the basis for bounding the types. This is where the fun starts.

Upper Bounded Wildcards

An upper bounded wildcard uses the extends keyword to set a superclass limit. To define a list of numbers that will allow ints, longs, doubles, and even BigDecimal instances to be added to it, see Example A-8.

Note

The keyword extends is used even if the upper bound is an interface rather than a class, as in List<? extends Comparable>.

Example A-8. A List with an upper bound
List<? extends Number> numbers = new ArrayList<>();
//        numbers.add(3);                    1
//        numbers.add(3.14159);
//        numbers.add(new BigDecimal("3"));
1

Still cannot add values

Well, that seemed like a good idea at the time. Unfortunately, while you can define the list with the upper bounded wildcard, again you can’t add to it. The problem is that when you retrieve the value, the compiler has no idea what type it is, only that it extends Number.

Still, you can define a method argument that takes List<? extends Number> and then invoke the method with the different types of lists. See Example A-9.

Example A-9. Using an upper bound
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));
}

Note that summing BigDecimal instances using their corresponding double values cancels the benefit of using big decimals in the first place, but only the primitive streams IntStream, LongStream, and DoubleStream include a sum method. This does illustrate the point, however, which is that you can invoke the method with lists of any subtype of Number. Since Number defines the doubleValue method, the code compiles and runs.

When you access an element from the list with an upper bound, the result can definitely be assigned to a reference of upper bound type, as in Example A-10.

Example A-10. Extracting a value from an upper bound reference
private static double sumList(List<? extends Number> list) {
    Number num = list.get(0);
    // ... from before ...
}

When the method is invoked, the elements of the list will be either Number or one of its descendants, so a Number reference will always be correct.

Lower Bounded Wildcards

A lower bounded wildcard means any ancestor of your class is acceptable. You use the super keyword with the wildcard to specify a lower bound. The implicationn, in the case of a List<? super Number>, is that the reference could represent List<Number> or List<Object>.

With the upper bound, we were specifying the type that the variables must conform to in order for the implementation of the method to work. To add up the numbers, we needed to be sure that the variables had a doubleValue method, which is defined in Number. All of the Number subclasses have that method as well, either directly or through an override. That’s why we specified List<? extends Number> for the input type.

Here, however, we’re taking the items from the list and adding them to a different collection. That destination collection could be a List<Number>, but it could also be a List<Object> because the individual Object references can be assigned to a Number.

Here’s the classic demonstration of the concept, which isn’t really idiomatic Java 8 code for reasons that will be discussed later, but does illustrate the concept.

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 A-11.

Example A-11. A method to populate a given list
public void numsUpTo(Integer num, List<? super Integer> output) {
    IntStream.rangeClosed(1, num)
             .forEach(output::add);
}

The reason this isn’t idiomatic Java 8 is that it’s using the supplied list as an output variable. That’s essentially a side effect, and therefore frowned upon. Still, by making the second argument of type List<? super Integer>, the supplied list can be of type List<Integer>, List<Number>, or even List<Object>, as in Example A-12.

Example A-12. Using the numsUpTo method
ArrayList<Integer> integerList = new ArrayList<>();
ArrayList<Number>  numberList = new ArrayList<>();
ArrayList<Object>  objectList = new ArrayList<>();

numsUpTo(5, integerList);
numsUpTo(5, numberList);
numsUpTo(5, objectList);

The returned lists all contain the numbers 1 through 5. The use of a lower bounded wildcard means we know the list is going to hold integers, but we can use references inside the list of any super type.

With the upper bounded list, we were extracting values and using them. With the lower bounded list, we supplied them. This combination has a traditional name: PECS.

PECS

The term PECS stands for “Producer Extends, Consumer Super,” which is an odd acronym coined by Joshua Bloch in his Effective Java book, but provides a mnemonic on what to do. It means that if a parameterized type represents a producer, use extends. If it represents a consumer, use super. If the parameter is both, don’t use wildcards at all—the only type that satisfies both requirements is the explicit type itself.

The advice boils down to:

  • Use extends when you only get values out of a data structure

  • Use super when you only put values into a data structure

  • Use an explicit type when you plan to do both

As long as we’re on the subject of terminology, there are formal terms for these concepts, which are frequently used in languages like Scala.

The term covariant preserves the ordering of types from more specific to more general. In Java, arrays are covariant because String[] is a subtype of Object[]. As we’ve seen, collections in Java are not covariant unless we use the extends keyword with a wildcard.

The term contravariant goes the other direction. In Java, that’s where we use the super keyword with a wildcard.

An invariant means that the type must be exactly as specified. All parameterized types in Java are invariant unless we use extends or super, meaning that if a method expects a List<Employee> then that’s what you have to supply. Neither a List<Object> nor a List<Salaried> will do.

The PECS rule is a restatement of the formal rule that a type constructor is contravariant in the input type and covariant in the output type. The idea is sometimes stated as “be liberal in what you accept and conservative in what you produce.”

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.

To keep things simple, Example A-13 shows an Employee POJO.

Example A-13. A trivial Employee POJO
public class Employee {
    private int id;
    private String name;

    public Employee(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // ... other methods ...
}

The code in Example A-14 creates a collection of employees, converts them into a Stream, and then uses the max method to find the employee with the max id and the max name (alphabetically3). The implementation uses anonymous inner classes to emphasize that the Comparator can be of type Employee or Object.

Example A-14. Finding the max Employee
List<Employee> employees = Arrays.asList(
    new Employee(1, "Seth Curry"),
    new Employee(2, "Kevin Durant"),
    new Employee(3, "Draymond Green"),
    new Employee(4, "Klay Thompson"));

Employee maxId = employees.stream()
    .max(new Comparator<Employee>() {        1
        @Override
        public int compare(Employee e1, Employee e2) {
            return e1.getId() - e2.getId();
        }
    }).orElse(Employee.DEFAULT_EMPLOYEE);

Employee maxName = employees.stream()
    .max(new Comparator<Object>() {          2
        @Override
        public int compare(Object o1, Object o2) {
            return o1.toString().compareTo(o2.toString());
        }
    }).orElse(Employee.DEFAULT_EMPLOYEE);

System.out.println(maxId);   3
System.out.println(maxName); 4
1

Anonymous inner class implementation of Comparator<Employee>

2

Anonymous inner class implementation of Comparator<Object>

3

Klay Thompson (max ID of 4)

4

Seth Curry (max name starts with S)

The idea is that the Comparator can be written taking advantage of methods in Employee, but it’s also legal to just use Object methods like toString. By defining the method in the API using the super wildcard, Comparator<? super T> comparator), either Comparator is allowed.

For the record, nobody would write that code that way any more. The more idiomatic approach is shown in Example A-15.

Example A-15. Idiomatic approach to finding the max Employee
import static java.util.Comparator.comparing;
import static java.util.Comparator.comparingInt;

// ... create employees ...

Employee maxId = employees.stream()
    .max(comparingInt(Employee::getId))
    .orElse(Employee.DEFAULT_EMPLOYEE);

Employee maxName = employees.stream()
    .max(comparing(Object::toString))
    .orElse(Employee.DEFAULT_EMPLOYEE);

System.out.println(maxId);
System.out.println(maxName);

This is certainly cleaner, but it doesn’t emphasize the bounded wildcard like the anonymous inner class.

Stream.map

As another simple example from the same class, consider the map method. It takes a Function with two arguments, both of which use wildcards:

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

The goal of this method is to apply the mapper function to each element of the stream (of type T) to transform it into an instance of type R.4 The return type from the map function is therefore Stream<R>.

Since Stream is defined as a generic class with type parameter T, the method doesn’t also need to define that variable in the signature. The method requires an additional type parameter, R, however, so that appears in the signature before the return type. If the class had not been generic, the method would have declared both parameters.

The java.util.function.Function interface defines two type parameters, the first (the input argument) is the type consumed from the Stream, and the second (the output argument) the type of object produced by the function. The wildcards imply that when the parameters are specified, the input parameter must be of the same type or above as the Stream. The output type can be any child of the returned stream type.

Note

The Function example looks confusing because from a PECS perspective, the types are backwards. However, if you keep in mind that Function<T,R> consumes a T and produces an R, it’s clearer why super goes on T and extends goes on R.

The code in Example A-16 shows how to use the method.

Example A-16. Mapping a List<Employee> to a List<String>
List<String> names = employees.stream()
    .map(Employee::getName)
    .collect(toList());

List<String> strings = employees.stream()
    .map(Object::toString)
    .collect(toList());

The Function declared two generic variables, one for input and one for output. In the first case, the method reference Employee::getName uses the Employee from the stream as input, and returns a String as output.

The second example shows that the input variable could have been treated as a method from Object rather than Employee, because of the super wildcard. The output type could, in principle, have been a List containing subclasses of String, but String is final so there aren’t any.

Next let’s look at one of the method signatures that introduced this appendix.

Comparator.comparing

The example in Example A-15 used the static comparing function from Comparator. The Comparator interface has been around since Java 1.0, so it might surprise developers to see it now has many additional methods. The Java 8 rule is that a functional interface is defined as an interface that contains only a single, abstract method (SAM). In the case of Comparator, that method is compare, which takes two arguments, both of generic type T, and returns an int that is negative, zero, or positive depending on whether the first argument is less than, equal to, or greater than, the second.5

The signature of comparing is:

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

Let’s start by breaking down the argument to the compare method, whose name is keyExtractor and is of type Function. As before, Function defines two generic types, one for the input and one for the output. In this case, the input is lower bounded by an input type T and the output is upper bounded by an output type U. The name of the argument is the key6 here: the function uses a method to extract a property to sort by, and the compare method returns a Comparator to do the job.

Because the goal is to use the ordering of a stream by the given property U, that property must implement Comparable. That’s why when U is declared, it must extend Compa⁠rable. Of course, Comparable itself is a typed interface, whose type would normally be U but is allowed to be any superclass of U.

Ultimately what the method returns is a Comparator<T>, which is then used by other methods in Stream to sort the stream into a new stream of the same type.

The code presented earlier in Example A-15 demonstrates how the method is used.

Map.Entry.comparingByKey and Map.Entry.comparingByValue

As a final example, consider adding the employees to a Map where the key is the employee ID and the values are the employees themselves. Then the code will sort them by ID or name, and print the results.

The first step, adding the employees to a Map, is actually a one-liner when you use the static toMap method from Collectors:

// Add employees to a map using id as key
Map<Integer, Employee> employeeMap = employees.stream()
    .collect(Collectors.toMap(Employee::getId, Function.identity()));

The signature of the Collectors.toMap method is:

static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
    Function<? super T,? extends K> keyMapper,
    Function<? super T,? extends U> valueMapper)

Collectors is a utility class (i.e., it contains only static methods) that produces implementations of the Collector interface.

In this example, the toMap method takes two arguments: one function to generate the keys and one function to generate the values in the output map. The return type is a Collector, which defines three generic arguments.

The Collector interface (from the Javadocs) has the signature:

public interface Collector<T,A,R>

Where the generic types are defined as:

  • T, the type of input elements to the reduction operation

  • A, the mutable accumulation type of the reduction operation (often hidden as an implementation detail)

  • R, the result of the reduction operation

In this case, we’re specifying the keyMapper, which is going to the getId method of Employee. That means here T is Integer. The result, R, is an implementation of the Map interface that uses Integer for K and Employee for U.

Then comes the fun part—the A variable in Collector is the actual implementation of the Map interface. It’s probably a HashMap,7 but we never know because the result is used as the argument to the toMap method so we never see it. In the Collector, though, the type uses an unbounded wildcard, ?, which tells us that internally it either only uses methods from Object or it uses methods in Map that aren’t specific to type. In fact, it only uses the new default merge method in Map, after calling the keyMapper and valueMapper functions.

To do the sorting, Java 8 has added static methods comparingByKey and comparingByValue to Map.Entry. Printing the elements sorted by key is shown in Example A-17.

Example A-17. Sorting Map elements by key and printing
Map<Integer, Employee> employeeMap = employees.stream()
    .collect(Collectors.toMap(Employee::getId, Function.identity())); 1

System.out.println("Sorted by key:");
employeeMap.entrySet().stream()
    .sorted(Map.Entry.comparingByKey())
    .forEach(entry -> {
        System.out.println(entry.getKey() + ": " + entry.getValue()); 2
    });
1

Add employees to a Map using ID as key

2

Sort employees by ID and print them

The signature of comparingByKey is:

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

The comparingByKey method takes no arguments and returns a Comparator that compares Map.Entry instances. Since we’re comparing by the keys, the declared generic type for the key, K, must be a subtype of Comparable that does the actual comparisons. Of course, the Comparable itself defines the generic type K or one of its ancestors, meaning that the compareTo method could use a property of the K class or above.

The result of this sorting is:

Sorted by key:
1: Seth Curry
2: Kevin Durant
3: Draymond Green
4: Klay Thompson

Sorting by value instead introduces a nice complication, where the error would be hard to understand without knowing something about the generic types involved. First, the signature of the comparingByValue method is:

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

This time it’s V that needs to be a subtype of Comparable.

A naïve implementation of sorting by value would then be:

// Sort employees by name and print them (DOES NOT COMPILE)
employeeMap.entrySet().stream()
    .sorted(Map.Entry.comparingByValue())
    .forEach(entry -> {
        System.out.println(entry.getKey() + ": " + entry.getValue());
    });

This doesn’t compile. The error you get is:

Java: incompatible types: inference variable V has incompatible bounds
    equality constraints: generics.Employee
    upper bounds: java.lang.Comparable<? super V>

The problem is that the values in the map are instances of Employee, and Employee doesn’t implement Comparable. Fortunately, the API defines an overloaded version of comparingByValue:

static <K,V> Comparator<Map.Entry<K,V>> comparingByValue(
    Comparator<? super V> cmp)

This method takes a Comparator as an argument and returns a new Comparator that compares the Map.Entry elements by the argument. The proper way to sort the map values is given in Example A-18.

Example A-18. Sorting map elements by value and printing
// Sort employees by name and print them
System.out.println("Sorted by name:");
employeeMap.entrySet().stream()
    .sorted(Map.Entry.comparingByValue(Comparator.comparing(Employee::getName)))
    .forEach(entry -> {
        System.out.println(entry.getKey() + ": " + entry.getValue());
    });

By providing the Employee::getName method reference to the comparing method, the employees are sorted by their names in their natural order:

Sorted by name:
3: Draymond Green
2: Kevin Durant
4: Klay Thompson
1: Seth Curry

Hopefully, these examples give you enough background on how to read and use the API without getting lost in the generics.

A Note on Type Erasure

One of the challenges of working on a language like Java is that it needs to support years of backward compatibility. When generics were added to the language, the decision was made to remove them during the compilation process. That way no new classes are created for parameterized types, so there is no runtime penalty to using them.

Since all of that is done under the hood, all you really need to know is that at compile time:

  • Bounded type parameters are replaced with their bounds

  • Unbounded type parameters are replaced with Object

  • Type casts are inserted where needed

  • Bridge methods are generated to preserve polymorphism

For types, the result is pretty simple. The Map interface defines two generic types: K for the keys and V for the values. When you instantiate a Map<Integer,Employee>, the compiler replaces K with Integer and V with Employee.

In the Map.Entry.comparingByKey example, the keys were declared such that K extends Comparable. Therefore everywhere in the class that uses K will be replaced by Comparable.

the Function interface defines two generic types, T and R, and has a single abstract method:

R apply(T t)

In Stream, the map method adds the bounds Function<? super T,? extends R>. So when we used that method:

List<String> names = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.toList());

The Function replaced T with Employee (since the stream was made up of employees) and R with String (since the return type from getName is String).

That’s basically it, leaving out some edge cases. See the Java Tutorial for details if you’re interested, but type erasure is probably the least complicated part of the whole technology.

Summary

The generics capabilities originally defined in J2SE 1.5 are still with us, but with the advent of Java 8 the corresponding method signatures have gotten far more complex. Most of the functional interfaces added to the language use both generic types and bounded wildcards to enforce type safety. Hopefully this appendix will give you the foundation you need to understand what the API is trying to accomplish, and therefore help you use it successfully.

1 Never once in my entire career did I accidentally add the wrong type to a list. Eliminating the casting on the way out, however, justified even this ugly syntax.

2 Java 10, known as Project Valhalla, has proposed adding primitive types to collections.

3 OK, technically it’s a lexicographical sort, meaning the capital letters come before the lowercase letters.

4 The Java API uses T for a single input variable, or T and U for two input variables, and so on. It normally uses R for return variables. For maps, the API uses K for the keys and V for the values.

5 Comparators are covered in Recipe 4.1.

6 Sorry.

7 In fact, in the reference implementation it is a HashMap.