Functional interfaces in Java

In our introduction to lambdas, we showed how a lambda expression could be used as a drop-in replacement for an interface implementation, using the specific example of the Comparator interface.

So instead of this:

	
Collections.sort(customers, new Comparator<>() {
	public int compare(Customer c1, Customer c2) {
		return c1.getName().compareTo(c2.getName());
	}
});

we could write this:

	
Collections.sort(customers, (c1, c2) -> c1.getName().compareTo(c2.getName()));

Note that no changes have been made to the Collections.sort() method to allow this to be possible. As far as the writer of the sort routine is concerned, they define a method that receives a Comparator instance and call the compare() method on that comparator. It is completely transparent to the writer of the sort method that when they invoke the compare() method, the code they are invoking may actually have been defined in a lambda expression rather than an actual concrete class that implements Comparator. To understand how and when this "compiler magic" is possible, we need to look in more detail at the notion of a functional interface.

What is a functional interface?

A functional interface is an interface where there is exactly one method that needs to be implemented. (For example, the Comparator interface has a single compare() method.) This means that when matching a lambda expression up to the corresponding interface, there is no ambiguity. Provided that an interface meets this criterion of having a single "required" method, it is automatically considered to be a functional interface by the Java compiler, and a lambda expression can be used instead of an implementation of the interface method in question. There are therefore a number of interfaces that existed prior to Java 8 which have automatically become functional interfaces. These include Comparator, Runnable and Callable, plus some less common examples such as FilenameFilter, ChangeListener and InvocationHandler.

So to take the example of the Runnable interface, instead of writing the following:

	
Thread t = new Thread(new Runnable() {
	public void run() {
		doInterestingThings();
	}
});

we may simply write:

	
Thread t = new Thread(() -> doInterestingThings());

Interfaces with "non required" meothds

When deciding if a particular interface is a functional interface, methods with default implementations do not count (hence the qualification of "needing to be implemented"). An interface could actually have more than one method and still class as a functional interface, provided that the "extra" methods have default implementations. Put another way, a lambda expression cannot stand in for a method with a default implementation. This also means that an interface with a single method is not a functional interface if that single method has a default implementation. In the rare circumstance that an interface re-declares one of the standard methods from the Object class such as equals(), these do not count either.

How to define a functional interface

In the cases above, we were able to use a lambda expression because there was an existing functional interface. In other words, there was an existing interface, such as Comparator, that defined a single method. The single method of the interface— compare() in this case— defined the parameter types and return type of the lambda expression. But what if we want to write a method that accepts a lambda expression, but where there is no pre-existing interface such as Comparator or Runnable for the combination of parameters and return type that we want that lambda expression to have?

One option is simply that we define the interface that we require. As an example, supposing that we want to write a method that iterates through all of the non-deleted items from a list of data items that we pass in, and returns a list of descriptions of those non-deleted items. We want the caller to be able to pass a lambda expression into the method which is used to determine the description for a given item. In other words, we would like to be able to make a call such as the following:

	
customerDescs = getItemDescriptions(customers, c -> c.getName() + " (" + c.getCustomerNo() + ")");

invoiceDescs = getItemDescriptions(invoices, inv -> inv.getInvoiceNo());

In these examples, he means of forming a description from a particular data item will be determined by the caller. But the overall logic of looping through the items and discarding those marked as deleted will be part of the method definition itself. Although a toy example, this represents a common use case for lambdas: where we want a general-purpose method that defines the "outer" process of iteration, while the "inner" process applied to each individual item processed is passed into the method as a lambda expression.

To achieve our goal, we can define a functional interface called DescriptionGetter, with a single getDescription() method:

	
public interface DescriptionGetter {

	public String getDescription(DataItem item);

}

Then, we can define our getItemDescriptions() method as follows:

	
public static List<String> getItemDescriptions(List<? extends DataItem> items, DescriptionGetter getter) {
	List<String> ret = new ArrayList(items.size());
	for (DataItem item : items) {
		String desc = getter.getDescription(item);
		ret.add(desc);
	}
	return ret;
}

A caller to getItemDescriptions() can now pass in either an implementation of DescriptionGetter or a lambda expression whose parameters match the signature of the getDescription() method: in other words, a lambda expression that takes a DataItem and returns a String, as in our examples above.

Next: general-purpose functional interfaces and the Stream API

You now have a good overview of the anatomy of lambda expressions in Java. Given a functional interface, you can write an equivalent implementation of that interface using a lambda expression. But arguably, this in itself would not make lambdas such a powerful feature:

To address these issues— and this is where lambdas become truly powerful in most practical cases— the Java platform has been extended to allow you to make more widespread use of lambdas out of the box:

There are also further details and rules of thumb that we will need to learn about as we become more proficient with lambda expressions.


If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.

Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.