Lambda Expressions and Functional Interfaces — How Java makes functions first-class citizens
Java is thought of as an Object-Oriented only language that is incredibly verbose. If you’ve ever worked on any moderately sized Java project, you’ve probably had nightmares about managing sprawling inheritance trees with BuilderFactoryProxy classes that initialise BuilderInstances via the ObserverBean that inherits from the coffee machine on the first floor. The amount of cognitive load legacy Java can carry is hefty. But, from Java 8 onwards, there has been a load of features released into the language that makes it much easier to understand and program with. This blog post will cover two of these: Functional Interfaces and Lambda Expressions.
What is a functional interface?
Functional interfaces are a feature introduced in Java 8 alongside lambdas. They allow functions to be treated as first-class citizens inside Java. Before this, you would have to create an inline anonymous class that contained the function implementation that’d look something like this.
This of course is very tricky to read and also somewhat unwieldy to manage. Using the new keyword with an interface also doesn’t make much sense as interfaces do not have constructors. Overall, this syntax is pretty poor ergonomically speaking and is something that will make even the cleanest of code hard to read. Thankfully, lambdas help a lot to tidy this up.
What is a Lambda Expression?
A Lambda Expression (or just Lambda) is very similar to a method in a lot of ways. It’s a short piece of code, that can take input parameters and return a value. They work in many of the same ways too, in that they need to be invoked before they do anything and can exist as members of an instance of a class. A key difference between Lambdas and methods is that a Lambda does not need to be named. It can be anonymous. It’s also possible to store lambdas in a variable too.
To break down this example is very simple. A lambda is comprised of the parameters it accepts and the code that it will execute. The parameters are supplied using a pair of parentheses, as you would do when creating a method and the code that will be executed is wrapped in curly braces. The key difference is the use of the ->
operator. This is what tells the compiler that what it is dealing with is a lambda expression.
The above example is an implementation of the Supplier Functional Interface. This is one of many different Functional Interfaces included in Java and is designed to allow the programmer to supply a value when invoked. Look at the docs here for more details on all the Functional Interfaces available.
https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
Other than some syntactic differences, Lambdas and methods are very similar in how they function. So when and why would we use Lambdas over regular methods? There are many massive benefits to using functional interfaces over methods, one of which was showcased in the above example: we can save implementations of methods without a class needing to be instantiated. This works because a Functional Interface is a Single Abstract Method Interface.
What is a Single Abstract Method Interface?
A Single Abstract Method (SAM) interface is an interface with a single abstract method. Easy, right? On the face of it, this is all a SAM is. But, if we delve into what interfaces can do, we can see that there is more nuance here than first appears.
From the above example, we can see that this SAM has 3 methods in it, 2 of which have implementations. If you understood interfaces as things that produce contracts for your code, you’d be right in thinking this. However, in Java, there are a few conveniences added to interfaces to make extending them simpler. Without going into too much detail on why interface methods can have visibility of default or static. If the method has this set visibility, it needs an implementation inside the interface. Once this is provided, the method can be used like a standard class method from the interface (in the case of a static reference) or from any object whose class implements the interface (in the case of a default method). What makes this interface a SAM is that there is only one method that needs implementation. So, if a class was to implement this interface, it’d only have to implement one method.
While this seems like a really obvious statement to make, there is one key point that makes this a game-changer. We can provide an implementation of the single abstract method with a lambda.
The above Functional Interface’s doIt() method is provided with the following implementation with a lambda. An interesting thing about lambdas is that the curly brackets are optional if the code being executed is only a single line. We can also leave out the return keyword too, implicitly returning the value. By using a Stream here, we can express a small amount of work on a collection in a single line. This lambda stored in the variable can now be passed around and used to produce the data output by the Stream by calling the doIt() method (it also has access to the default methods from the interface too). This is of course really cool, especially for a language like Java that is renowned for its verbosity. But, how do we actually apply this?
Application
Let’s assume we have a little app that does some counting for us. In this app, we have the contracts of our app defined as interfaces. An interface for our service that actually does all the work is called CanCount and its implementation, CountService. These are pretty standard and don’t have any real surprises in them.
A key part of the CountService is that it has a method that accepts a Counting class instance. This is a functional interface that looks like this.
As mentioned previously, functional interfaces need to have only one abstract method. An annotation here (the thing with the @ symbol above the interface) tells the Java compiler that this interface is a functional interface. This isn’t required but does make anyone consuming the interface aware of its purpose. It will also cause an error if the interface somehow picks up another abstract method.
The final interface is the Job interface. This has a single abstract method again but is not supposed to be a functional interface. Theoretically, the Job interface could be used in this way without issue. However, this is not the intent of this interface. By leaving the annotation off, we let anyone looking at this code know what the intent of this interface is. The Job also leverages generics to ensure that it can be applied to whatever Job we want to do. In our app, this Job only handles Jobs that produce Count objects but could easily be used for other types of jobs we want to do.
Our Count class is simply to be used to hold values. Rather than passing around a list of integers that our CountService produces.
Finally, the CountSomeThings class that implements the Job interface is what makes the magic happen. CountSomeThings has an instance of the CountService to do all the work (this isn’t actually required but keeps all our work encapsulated in one class) and accepts a lambda in its constructor. The execute method then passes this lambda to the CountService to handle its execution, then returns the result wrapped in a new Count.
Now, how do we actually use all of this stuff and what does this have to do with Functional Interfaces and Lambdas?
Code in action
Let’s only focus on main.java as this is where we are truly showing off the power of Functional Interfaces. The myCounting variable holds an implementation of the Counting Functional Interface that uses one of the methods referenced on the interface in its implementation. This now means we have what is essentially a first-class function inside of a Java program. To invoke it, we simply need to call the doIt() method.
Instead of doing this directly though, let’s use the mechanism we built (our Job class) to handle the invocation of this function.
While this is a much more terse way of handling code, it can be somewhat confusing to someone who hasn’t seen it before. Luckily, the use of Functional Interfaces does not box you into using them a certain way. We can still create a class that implements this interface as we would do normally.
As we can see from the NewThing class, we are referencing one of the methods on the interface, this time the default method. As the class implements the interface, it has access to the default methods and can override their implementation too if needed.
Finally, the reason lambdas and Functional Interfaces are so powerful is that you can pass a lambda when a Functional Interface implementation is expected.
As mentioned, as long as a lambda satisfies the contract specified by the functional interface, it is a valid implementation. As the CountSomeThings class expects a valid implementation of a Counting, anything from an anonymous lambda to an object whose class implements the interface is a valid argument. This has some very powerful benefits:
- Our CountSomeThings Job can essentially do whatever we need it to without having to create any new classes or methods
- Our implementations for Counting can vary and have no effect on the running of our program. We’ve decoupled the logic for our Counting from the Job that produces it.
- We can pass around expressions to Counting objects, meaning we can tend towards immutability and treat our functions as first-class citizens.
- Our code is more declarative and expressive, letting us reason about our system much easier.
Functional Interfaces also allow us to enforce contracts on behaviour on pieces of our code in a way that defining return types and using plain interfaces simply cannot do. Returning specific types in interfaces can lead to leaky abstractions if they are fluent when they shouldn’t be for example. By using a Functional Interface, you control exactly how code should behave as well as what values you get back from its execution. Truly, lambdas and Functional Interfaces allow Java to reach a new level of expressiveness and code clarity.
Summary
To summarise:
- Functional Interfaces are SAM interfaces that can specify the behaviour of anything that implements them.
- Lambdas can be used to implement SAM interfaces allowing the programmer to pass references to functions making them first-class.
- Using Functional Interfaces in this way allows programmers to leverage declarative programming, expressing work in a succinct and readable way.
Code for what was in this blog post is here:
Feel free to fork and have a play around :).