A key concept in functional programming is the notion of higher order functions. Higher order functions are functions that can be used as input parameters and output values for other functions or methods. However in a typed language like Java we need to be able to define our parameters and return values as types.
When defining an integer parameter, we use the type int
. For a collection of string values we can use Collection<String>
or we can be more specific with types like List<String>
or Set<String>
. To benefit from the use of higher order functions, we need to be able to describe a lambda expression or method reference in a similar way.
Functional interfaces are a language feature added in Java 8 along with lambda expressions that provide a means for expressing a lambda or method as a specific type by using an interface. Any interface with a single abstract method can be considered a functional interface and can be used in the type signature of a method that expects a lambda expression as a parameter or output value.
Video Rental Inventory Management
To illustrate how functional interfaces work and the usefulness of higher order functions, let's create a simple inventory management system. Video rental stores have predominately become a thing of the past, but for those that still exist, keeping track of available movie inventory is a must. The requirements for this application are as follows:
- It should be able to count the available inventory of all movies.
- It should be able to count movies by media type — either VHS or DVD.
- It should be able to count movies by title.
- It should be extensible for including additional inventory counting methods in the future.
First we define a couple of domain models for Movie
and Inventory
using Lombok to reduce the amount of boilerplate required.
public enum MediaType {
VHS, DVD
}
@Data
public class Movie {
private final String title;
private final MediaType mediaType;
private final int quantity;
}
@RequiredArgsConstructor
public class Inventory {
private final List<Movie> movies;
}
We could add a method to the Inventory
class with the logic for counting available movies however with each inventory counting requirement we would either need to write separate methods or utilize object composition.
Writing separate methods would mean our class is not closed for modification, violating one of the SOLID principles. Object composition would be preferable, but it would require a new interface to be made and added as a field on the Inventory
class as well as the creation of new classes that implement the interface for every known and future requirement.
// adding new methods for each requirement modifies the class's public contract with each new use case
@RequiredArgsConstructor
public class Inventory {
private final List<Movie> movies;
public int countAll() {
return movies.stream().mapToInt(Movie::getQuantity).sum();
}
public int countAllVhs() {
return movies.stream()
.filter(movie -> Objects.equals(movie.getMediaType(), MediaType.VHS))
.mapToInt(Movie::getQuantity).sum();
}
public int countAllByTitle(String title) {
return movies.stream()
.filter(movie -> Objects.equals(movie.getTitle(), title))
.mapToInt(Movie::getQuantity).sum();
}
}
// object composition is preferable but requires new classes to be made for each use case
public interface InventoryCounter {
int count(List<Movie> inventory);
}
@RequiredArgsConstructor
public class Inventory {
private final List<Movie> movies;
private final InventoryCounter counter;
public int count() {
return counter.count(movies);
}
}
Leveraging Higher Order Functions
Higher order functions provide us with a third option. We can define the interface of a function that takes a list of movies and returns an integer value, which at a high level accounts for the functionality of all our movie counting use cases. We use the @FunctionalInterface
annotation to document our intended purpose for the InventoryCounter
interface.
@FunctionalInterface
public interface InventoryCounter {
int apply(List<Movie> inventory);
}
Other than changing the name of the single function on this interface, it is identical to the interface in the object composition example above. However while the interface itself has not changed, the way we use it has.
Next we add a count
method to the Inventory
class. As its only parameter, this method accepts a lambda function or method reference with the same type signature as the apply
method on the interface; any method or lambda that has List<Movie>
as an input parameter and int
as an output value.
@RequiredArgsConstructor
public class Inventory {
private final List<Movie> movies;
public int count(InventoryCounter counter) {
return counter.apply(movies);
}
}
Testing the Implementation
Now we can test our solution. As if renting physical copies of movies wasn't niche enough, our particular client deals only in movies from the DC cinematic universe.
public class InventoryCounterTest {
private static Inventory inventory;
@BeforeAll
public static void setup() {
inventory = new Inventory(getMovies());
}
private static List<Movie> getMovies() {
return asList(
new Movie("Aquaman", MediaType.DVD, 15),
new Movie("Aquaman", MediaType.VHS, 5),
new Movie("Batman v. Superman", MediaType.DVD, 25),
new Movie("Batman v. Superman", MediaType.VHS, 10),
new Movie("Justice League", MediaType.DVD, 30),
new Movie("Justice League", MediaType.VHS, 12),
new Movie("Man of Steel", MediaType.DVD, 12),
new Movie("Man of Steel", MediaType.VHS, 3),
new Movie("Wonder Woman", MediaType.DVD, 35),
new Movie("Wonder Woman", MediaType.VHS, 10)
);
}
}
How they managed to acquire VHS copies of those films is a mystery.
Our first use case is to count the total number of movies available. To accomplish this we can use an anonymous lambda expression, a lambda expression assigned to a variable, or a method reference.
// anonymous lambda expression
@Test
public void itShouldCountAllMoviesInInventory() {
var inventoryCount = inventory.count(movies -> movies.stream()
.mapToInt(Movie::getQuantity)
.sum());
assertEquals(157, inventoryCount);
}
// lambda expression assigned to a variable
@Test
public void itShouldCountAllMoviesInInventory() {
InventoryCounter counter = movies -> movies.stream()
.mapToInt(Movie::getQuantity)
.sum();
var inventoryCount = inventory.count(counter);
assertEquals(157, inventoryCount);
}
// method reference
@Test
public void itShouldCountAllMoviesInInventory() {
var inventoryCount = inventory.count(this::countMovieInventory);
assertEquals(157, inventoryCount);
}
private int countMovieInventory(List<Movie> movies) {
return movies.stream().mapToInt(Movie::getQuantity).sum();
}
We can easily test the second requirement — counting the available VHS movies — this time using only the method reference approach. This is the preferred way of satisfying a functional interface for anything other than short lambda expressions with intent easily derived from the expression itself.
When in doubt, favor a well-named method reference.
@Test
public void itShouldCountAllVhsMoviesInInventory() {
var vhsInventoryCount = inventory.count(this::countVhsMovieInventory);
assertEquals(40, vhsInventoryCount);
}
private int countVhsMovieInventory(List<Movie> movies) {
return movies.stream()
.filter(movie -> Objects.equals(movie.getMediaType(), MediaType.VHS))
.mapToInt(Movie::getQuantity).sum();
}
We have one last requirement to fulfill — counting the available movies by title. To meet this requirement we need to use the InventoryCounter
functional interfaces in a new way, as the return type of a method.
@Test
public void itShouldCountAllMoviesInInventoryWithSpecifiedTitle() {
var aquamanInventoryCount = inventory.count(countMovieInventoryWithTitle("Aquaman"));
var wonderWomanInventoryCount = inventory.count(countMovieInventoryWithTitle("Wonder Woman"));
assertEquals(20, aquamanInventoryCount);
assertEquals(45, wonderWomanInventoryCount);
}
private InventoryCounter countMovieInventoryWithTitle(String title) {
return movies -> movies.stream()
.filter(movie -> Objects.equals(movie.getTitle(), title))
.mapToInt(Movie::getQuantity).sum();
}
The countMovieInventoryWithTitle
method cannot be used as a method reference like in the previous two examples because it does not satisfy the InventoryCounter
functional interface. What it does do is return a lambda expression that satisfies the functional interface and allows us to include some dynamic information in the lambda, the title of the movie.
With this last test we have met all of our requirements. The three known use cases are covered and by using functional interfaces we have ensured our code is extensible for future use cases.
Standard Functional Interfaces
We have eliminated a lot of unnecessary code by using functional interfaces over object composition, however there is one opportunity to refactor that allows us to remove the InventoryCounter
interface. Most function type signatures can be expressed by a small number of interfaces with one or more generic type parameter. The function
package leverages this fact to provide several standard functional interfaces so that you do not have to define your own.
import java.util.function.Function;
import java.util.function.ToIntFunction;
public class Inventory {
private final List<Movie> movies;
// the Function interface accepts two type parameters, one for the input parameter and one for the output
public int count(Function<List<Movie>, Integer> counter) {
return counter.apply(movies);
}
// to avoid unnecessary boxing, each functional interface has several alternatives for working with primitives
public int count(ToIntFunction<List<Movie>> counter) {
return counter.applyAsInt(movies);
}
}
There are times it may be necessary or more expressive to define your own functional interfaces, but for all other use cases, you should use the standard functional interfaces.
More to Explore
Although this introduction to and overview of functional interfaces has come to an end, there is a whole lot more to explore. In future blog posts I will review each of the primary functional interface types, describing their use cases, providing code examples of how they work, and exploring additional functionality exposed by their individual APIs.
Top comments (1)
This is excellent write up. Easy to understand Functional Interface. But your code snippets wont compile as you are using the same snippet to mix concepts.