Monday, April 06, 2015

Functional interfaces in JDK 8

After my previous post on Lambdas, I decided to have a closer look at what makes a functional interface.

What is a functional interface?

  1. A functional interface is an interface that has one abstract method.
    1. Functional interfaces used to be called Single Abstract Method (SAM) interfaces.
  2. A functional interface must be defined as an interface type, not an annotation type, enum, or class.
  3. A functional interface can optionally be annotated with the @FunctionalInterface annotation.
    1. This annotation is not necessary for Lambdas as the compiler will figure out if an interface is functional or not (by seeing if it has only one abstract method). The annotation is useful when you are writing a functional interface, because the compiler will generate an error if the interface you are writing does not meet the requirements of being a functional interface.

There are a number of conditions around what counts as the single abstract method in a functional interface.

  1. JDK 8 allows you to add concrete methods to interfaces by using the default keyword. These default methods do not count.
  2. JDK 8 allows you to add static methods to interfaces. These do not count.
  3. Interfaces can also re-declare (override) methods from java.lang.Object and while these methods are also abstract in an interface, they don't count either because any implementation of the interface will automatically inherit those methods from Object. A good example of this is Comparator, which re-declares the equals method so that it can put special notes about it in the javadocs (see: why does comparator declare equals?).
  4. An advanced case involves a functional interface that extends from multiple interfaces that include override-equivalent methods (methods that have the same signature after type erasure). See below.

Functional interface as target type for a lambda

Functional interfaces are much more powerful in JDK 8, where they are used as the target type for lambdas. Whenever you create a lambda in JDK 8:

  1. The compiler will figure out what functional interface fits the parameter list and return type for that lambda.
  2. It will create the lambda as an instance of that functional interface (an object whose type is that of the interface).
  3. The code within the lambda will be used as the concrete implementation of the sole abstract method.

An example of a functional interface being assigned to a lamba is below. First we have a simple POJO representing a book.

public final class Book {
   private final String title;
   private final String author;

   public Book(final String theAuthor, final String theTitle) {
      title = theTitle;
      author = theAuthor;
   }

   public String getAuthor() { return author; }

   public String getTitle() { return title; }

   @Override
   public String toString() { return title + " by " + author; }
}

Then we have a class that will create a few Book instances and add them to a list. After that, the code uses two lambdas to sort and then print books. The last two lines invoke methods on ArrayList that accept functional interface type parameters. We use lambdas to provide them.

public final class FunctionalInterfaceTest {
   public static void main(String[] args) {
      List<Book> books = new ArrayList<Book>();
      books.add(new Book("Stephen King", "The Shining"));
      books.add(new Book("Bram Stoker", "Dracula"));
      books.add(new Book("Thomas Harris", "The Silence of the Lambs"));
      books.add(new Book("Henry James", "The Turn of the Screw"));
      books.add(new Book("David Wong", "John Dies at the End"));
      books.add(new Book("Ryu Murakami", "Piercing"));
      books.add(new Book("Peter Straub", "Ghost Story"));
      // Sort books by title.
      books.sort((book1, book2) -> book1.getTitle().compareTo(book2.getTitle()));
      // Print books in their new order.
      books.forEach((book) -> System.out.println(book));
   }
}

The output of this code is:

Dracula by Bram Stoker
Ghost Story by Peter Straub
John Dies at the End by David Wong
Piercing by Ryu Murakami
The Shining by Stephen King
The Silence of the Lambs by Thomas Harris
The Turn of the Screw by Henry James

Here is line that sorts the books.

// Sort books by title.
books.sort((book1, book2) -> book1.getTitle().compareTo(book2.getTitle()));

The argument to ArrayList's sort method must be a Comparator, which is a functional interface. It's sole abstract method is compare(T o1, T o2) where o1 and o2 are not specified but must be the same type; also, the method returns an int. This is what the compiler expects us to provide as a parameter and the lambda we have written will fit that type.

Looking a bit closer into the lambda: (book1, book2) -> book1.getTitle().compareTo(book2.getTitle()).

  1. (book1, book2)
    1. The parameter list contains two arguments whose type we do not specify.
    2. The compiler will infer them as being two Book objects because we are calling sort on a list of books (List<Book>).
  2. book1.getTitle().compareTo(book2.getTitle())
    1. The code to execute in the lambda is an expression - a piece of code that will evaluate to a single value. The value in this case is an int because compareTo on String returns an int.
    2. This matches the sole abstract method in Comparator, which is compareTo. So the compiler will consider this lambda to be an instance of the functional interface Comparator.
    3. Somewhere in the sort code, compareTo will be called on our lambda, which is now a Comparator object.

Here is line that prints the books.

// Print books in their new order.
books.forEach((book) -> System.out.println(book));

The argument to ArrayList's forEach method must be a Consumer, which is a functional interface. It's sole abstract method is accept(T t) where t is some non-specific type and the method has a void return type. This is what the compiler expects us to provide as a parameter and the lambda we have written will fit that type.

Looking a bit closer into the lambda: (book) -> System.out.println(book).

  1. (book)
    1. The parameter list contains one argument whose type we do not specify.
    2. The compiler will infer that it is a Book object because we are calling forEach on a list of books (List<Book>).
  2. System.out.println(book)
    1. The code to execute in the lambda is a single statement, with a void return type. Since we don't use the return keyword, compiler figures out that nothing is being returned and thus the lambda code has a void return type.
    2. This matches the sole abstract method in Consumer, which is accept. So the compiler will consider this lambda to be an instance of the functional interface Consumer.
    3. Somewhere in the forEach code, accept will be called on our lambda, which is now a Consumer object

Functional interfaces and override-equivalent methods

With respect to functional interfaces and lambdas, this is a corner case. However, I am going into it in further detail here because it reveals much about the implications of type erasure that came with Generics in JDK 5. The question: what happens to a functional interface that extends from multiple interfaces that contain override-equivalent methods i.e. methods that have the same signature after type erasure?

First, a look at type erasure, which occurs when generic type information is removed when the compiler generates a class file from source Java. This is done so that code which uses generics will still be compatible with pre-JDK 5 code that doesn't use generics. Practically speaking, it means that you cannot have two methods like this in the same class:

public void foo(List bar) { }
public void foo(List<String> bar) { }

This code will not compile because after type erasure, they would have exactly the same signature:

public void foo(List bar) { }
public void foo(List bar) { }

The above methods are override-equivalent: their signatures are the same after type erasure. JLS (Java Language Specification), Chapter 8. Classes - 8.4.2. Method Signature says that two methods are override-equivalent if they have the same signature (name and parameter list) or if they have the same signature after type erasure.

While you can't put two override-equivalent methods in a single class, you can legally end up inheriting from multiple interfaces that contain override-equivalent methods. The result will be a method that can legally override all the inherited abstract methods (after type erasure). The example below shows what happens when an interface extends other interfaces (functional interfaces in this case because they have only one abstract method each) whose sole methods are all override-equivalent: in fact, two of them are exactly the same before type erasure.

interface Foo1 { void bar(List<String> arg); }
interface Foo2 { void bar(List<String> arg); }
interface Foo3 { void bar(List arg); }
@FunctionalInterface interface Foo extends Foo1, Foo2, Foo3 {}
public class OverrideEquivalent implements Foo {
  // This compiles.
  @Override public void bar(List arg) { }
  // Does not compile if we use this one instead.
  // @Override public void bar(List<String> arg) { }
}

The example above shows that a method without generics can legally override generic methods that will have the same signature after type erasure, or non-generic methods that are the same signature. If you use Foo as a functional interface, the method you end up overriding with will be the one that can override all the others i.e. it will have all the generic types erased.

References about type erasure:

  1. JSL, Chapter 4. Types, Values, and Variables - 4.6. Type Erasure.
  2. The Java Tutorials - Type Erasure.
  3. This Stack Overflow post: Java generics - type erasure - when and what happens.
    1. It features this answer by WChargin which explains how code that uses generics like this:
      List<String> list = new ArrayList<String>();
      list.add("Hi");
      
      is compiled into the same code but with generic type information removed:
      List list = new ArrayList();
      list.add("Hi");
      
      It also points out that there is still metadata in the class file about generics, but it is not accessible to code that uses the class file: they are converted into compile-time checks and runtime casts.
  4. Another answer on the same question shows a way to get around type erasure with anonymous classes, which is elaborated on further here:
    1. Super Type Tokens.
    2. Using TypeTokens to retrieve generic parameters.