Understanding Java's functional interfaces
Introduction
"Functional interfaces" are interfaces whose implementation can be provided by a lambda expression. That is, if a method has such an interface as a parameter then, rather than defining a class that implements the interface, we can just specify an operation in the method call body. When used carefully, this technique allows for compact, expressive code.
If that all sounds rather abstract, don't worry -- in this article I will provide a specific, simple example that demonstrates where such a technique could be useful.
Functional interfaces have been available since Java 8 but, in my experience, they aren't widely used outside the standard runtime library. To be fair, the syntax can look a little peculiar at first. Many examples to be found in the standard runtime library are not easy to comprehend, because they combine functional interfaces with other rather abstract techniques, such as parameterized classes. While parameterized classes are important in their own right, this article does not require any understanding of parameterized classes. I also don't assume any knowledge of lambda functions -- something I can really only get away with because my examples are so simple. To use functional interfaces effectively, you really do have to understand how to use lambda expressions.
Stating the problem
Consider a class Message
that represents some kind of message
in a communication system. The Message
class
has a sign()
method, that stores or applies some sort of
digital signature to the message. The Message
class has
many complex methods, but all we're concerned with here are the methods
sign()
, and getText()
. This latter method
simply returns a textual representation of the message body. So, in
outline, the Message
class looks like this.
class Message { // Many other methods... // Sign a message using the specified signer public void sign (Signer t) { String sig = t.getSignature (this); // Store the signature ... } // Return the message body public String getText () { return ...; }; }
The sign()
method does not
actually generate a digital signature -- it just delegates that to
something called Signer
. This is an interface that defines
one method, getSignature()
:
iinterface Signer { public String getSignature (Message m); };
The message-signing logic might look like this:
Message m = new Message ("Hello"); m.sign (/* ??? */);
The question, then, is: how to provide the argument to
m.sign()
? We anticipate that the application might have
several implementations of the Signer
interface, so
that different methods of message signing can be provided.
The problem is to provide implementations that are both compact, and
expressive.
Pre-Java 8 implementations
The most natural, comprehensible implementation, which will work
with any Java going back to JDK1.1, is to define
specific classes that implement the Signer
interface.
Consider a very trivial implementation called HashSigner
that generates a signature from a hash of the message body.
Here's how HashSigner
might be implemented.
class HashSigner implements Signer { @Override public String getSignature (Message m) { // Crude implementation return "" + m.getText().hashCode(); } }
Since HashSigner
implements the Signer
interface, the sign()
function in Message
can call the getSignature()
method via the interface.
The Message
class need have no knowledge of how
HashSigher
works, or even that it exists -- that's the
power of interfaces.
A problem with this implementation is the amount of boilerplate code needed for what is, in the end, a trivially simple operation. It's easy to understand but, in many cases, the actual logic becomes swamped with scaffolding code that plays no real part in the implementation.
There is an alternative formulation using anonymous inner classes that has also been available for a very long time. This method of implementing an interface without a named class is hugely popular, and almost any substantial Java program or library will use it extensively. Here's how we can perform the same, trivial message-signing logic without defining a new class:
m.sign (new Signer () { public String getSignature (Message m) { return "" + m.getText().hashCode(); } });
Notice that the implementation logic here is exactly the same
as the previous example. Moreover, this implementation does
define a class that implements the Signer
interface, but
it is anonymous. The compiler will generate a .class
file
with a machine-generated name.
The problem with this formulation is that it uses only a little less boilerplate than the conventional one, and the syntax is not particularly elegant. Nevertheless, it's rare to look at any substantial Java project, prior to Java 8, that does not have hundreds of examples of this kind of coding.
Java 8 and later -- using lambda functions
Java 8 and later provide a much more compact representation, using functional interfaces and lambda functions. This isn't really the place to provide a detailed description of a lambda function but, essentially, a lambda function (also known as a closure or an anonymous function) is a specification of code to be executed on particular arguments. Traditionally, of course, "code to be executed on particular arguments" would amount precisely to a function. A lambda function, however, is like a function implementation without the function definition.
Consider this very simple function:
double sqr (double x) { return x * x; }
This function takes a double
argument, and returns the
square of its argument by multiplying it by itself. So far, so ordinary.
A lambda expression that defines the same behaviour might be:
(a)->(a*a)
This expression doesn't have a name, so it's not a full function. We can't
call it -- not in this form, anyway. It takes one argument a
,
and we need not define the type of the argument if the
compiler can work it out
from the implementation.
Here's how
the message signing function can be implemented using a lambda expression:
m.sign (a -> "" + a.getText().hashCode());You'll see that this formulation has no boilerplate -- just the logic. The logic itself -- calling
hashCode()
on the
message body is exactly the same as in the previous two examples.
Here, though, there is no scaffolding -- not even an anonymous class.
But how does this formulation actually work? The method
Message.sign()
expects a class that implements the Signer
interface.
Where's the class? How does the JVM know that the supplied lambda expression
is, in some way, an implementer of the Signer
interface?
The interface Signer
is a functional interface. That is,
it is an interface with a structure that makes it applicable to use in
lambda formulations. Notice that the interface only defines one
method so it can, in theory, be invoked without specifying the method
name -- there's no chance of ambiguity. This single method
-- getSignature()
-- takes one argument of type Message
. So a lambda expression
can legitimately be used in place of an implementation of
Signer
, so long as the lambda expression takes one argument
that can be interpreted as taking a Message
argument.
In our example, The parameter of the lambda expression is simply
a
, defined without any type. However, the operation to be applied
to it -- a.getText()
is a method call on
a -> "" + a.getText().hashCode())
can be used passed as an argument to any function that takes as
a parameter any
functional interface that specifies a method that takes a
Message
argument.
There's no doubt that this is a compact, elegant way to call a function with a specified operation as its argument. It's not necessarily a technique to be used with abandon, as I'll explain later; but it's much more concise than the use of anonymous inner classes -- at least where the specified operation is relatively easy to express.
What makes a functional interface?
Java 8 and later define a built-in annotation @FunctionalInterface
that can be used to mark an interface as being functional. However,
it's not the annotation -- which is optional -- that is significant,
but the structure. A functional interface defines exactly one
abstract method. Actually that's not exactly true, but the
subtleties need not worry us here. This definition of
Signer
would fail:
@FunctionalInterface interface Signer { public String getSignature (Message m); public String foo (int f); };With the
@FunctionalInterface
annotation in place,
the definition would fail, whether or not Signer
was
actually used elsewhere in the code. The error message would be
of this form:
error: Unexpected @FunctionalInterface annotation @FunctionalInterface ^ Signer is not a functional interface multiple non-overriding abstract methods found in interface Signer
Without the annotation, the compiler would eventually raise an error if the
code tried to use Signer
as a functional interface --
that is, if it tried to use a lambda expression to stand in for the
interface.
For the sake of completeness, I should point out that to be a functional
interface only places restrictions on abstract methods, that is,
methods that require to be implemented in some class. There are no
restrictions on static
or default
methods.
Closing remarks
Functional interfaces open the way to using a whole new programming paradigm with Java. They allow for compact code which has some expressive power. However, functional interfaces are potentially a cause of confusion. The main problem is that there's no way to look at a piece of code like
m.sign (a -> "" + a.getText().hashCode());
and to see immediately what use will be made of the lambda expression.
Compare this with the example that used an anonymous inner class: with
the anonymous inner class
it was perfectly clear that we were invoking a method
called getSignature()
on a thing called Signer
.
This is less obvious when the interface itself is non-obvious in the code.
Perhaps clarity would be improved if I wrote
m.sign (Message a -> "" + a.getText().hashCode());
or
m.sign (a_message -> "" + a_message.getText().hashCode());
In any case, the use of functional interfaces creates opportunities to think hard about the expressive power of names, and where additional comments might be appropriate. Carelessness in this area is not really a problem when you're writing code -- it's just regrettable when you come to fix a bug in it five years later.