Home > Articles > Programming > Java

  • Print
  • + Share This
This chapter is from the book

3.9. Lambdas and Generics

Generally, lambdas work well with generic types. You have seen a number of examples where we wrote generic mechanisms, such as the unchecked method of the preceding section. There are just a couple of issues to keep in mind.

One of the unhappy consequences of type erasure is that you cannot construct a generic array at runtime. For example, the toArray() method of Collection<T> and Stream<T> cannot call T[] result = new T[n]. Therefore, these methods return Object[] arrays. In the past, the solution was to provide a second method that accepts an array. That array was either filled or used to create a new one via reflection. For example, Collection<T> has a method toArray(T[] a). With lambdas, you have a new option, namely to pass the constructor. That is what you do with streams:

String[] result = words.toArray(String[]::new);

When you implement such a method, the constructor expression is an IntFunction<T[]>, since the size of the array is passed to the constructor. In your code, you call T[] result = constr.apply(n).

In this regard, lambdas help you overcome a limitation of generic types. Unfortunately, in another common situtation lambdas suffer from a different limitation. To understand the problem, recall the concept of type variance.

Suppose Employee is a subtype of Person. Is a List<Employee> a special case of a List<Person>? It seems that it should be. But actually, it would be unsound. Consider this code:

List<Employee> staff = ...;
List<Person> tenants = staff; // Not legal, but suppose it was
tenants.add(new Person("John Q. Public")); // Adds Person to staff!

Note that staff and tenants are references to the same list. To make this type error impossible, we must disallow the conversion from List<Employee> to List<Person>. We say that the type parameter T of List<T> is invariant.

If List was immutable, as it is in a functional programming language, then the problem would disappear, and one could have a covariant list. That is what is done in languages such as Scala. However, when generics were invented, Java had very few immutable generic classes, and the language designers instead embraced a different concept: use-site variance, or “wildcards.”

A method can decide to accept a List<? extends Person> if it only reads from the list. Then you can pass either a List<Person> or a List<Employee>. Or it can accept a List<? super Employee> if it only writes to the list. It is okay to write employees into a List<Person>, so you can pass such a list. In general, reading is covariant (subtypes are okay) and writing is contravariant (supertypes are okay). Use-site variance is just right for mutable data structures. It gives each service the choice which variance, if any, is appropriate.

However, for function types, use-site variance is a hassle. A function type is always contravariant in its arguments and covariant in its return value. For example, if you have a Function<Person, Employee>, you can safely pass it on to someone who needs a Function<Employee, Person>. They will only call it with employees, whereas your function can handle any person. They will expect the function to return a person, and you give them something even better.

In Java, when you declare a generic functional interface, you can’t specify that function arguments are always contravariant and return types always covariant. Instead, you have to repeat it for each use. For example, look at the javadoc for Stream<T>:

void forEach(Consumer<? super T> action)
Stream<T> filter(Predicate<? super T> predicate)
<R> Stream<R> map(Function<? super T, ? extends R> mapper)

The general rule is that you use super for argument types, extends for return types. That way, you can pass a Consumer<Object> to forEach on a Stream<String>. If it is willing to consume any object, surely it can consume strings.

But the wildcards are not always there. Look at

T reduce(T identity, BinaryOperator<T> accumulator)

Since T is the argument and return type of BinaryOperator, the type does not vary. In effect, the contravariance and covariance cancel each other out.

As the implementor of a method that accepts lambda expressions with generic types, you simply add ? super to any argument type that is not also a return type, and ? extends to any return type that is not also an argument type.

For example, consider the doInOrderAsync method of the preceding section. Instead of

public static <T> void doInOrderAsync(Supplier<T> first, 
   Consumer<T> second, Consumer<Throwable> handler)

it should be

public static <T> void doInOrderAsync(Supplier<? extends T> first, 
   Consumer<? super T> second, Consumer<? super Throwable> handler)
  • + Share This
  • 🔖 Save To Your Account