In the previous part, we learned about Java’s functional capabilities. This time, we’ll go over the functional interfaces which are included in the JDK since Java 8’s introduction of lambdas.
java.util.function` provides 43 functional interfaces, containing all the building blocks needed to create quite complex functional code.
The Big Four
The four functional interfaces that we’ll likely use the most are:
Takes no arguments, but returns an object. A method reference to a simple POJO-Getter qualifies as a
void Consumer<T>#accept(T t)
Takes a single argument and doesn’t return anything. Every POJO-Setter qualifies as a
R Function<T, R>#apply(T t)
Takes a single argument and returns an object.
boolean Predicate<T>#test(T t)
A specialized function that accepts an object and returns a
With just these four alone we can do a lot of functional programming:
Lambdas often work with the same type as argument(s) and return type.
With Java being a verbose language, we don’t want to write the parameterized types all the time. To avoid this, Java provides more specialized interfaces with more straightforward generic signatures:
Arity | Specialized | Super-Interface -------+-------------------+------------------- 1 | UnaryOperator<T> | Function<T,T> 2 | BiConsumer<T,U> | - 2 | BiFunction<T,U,R> | - 2 | BinaryOperator<T> | BiFunction<T,T,T> 2 | BiPredicate<T,U> | -
Be aware that instead of using the
...Operator<T> as arguments in a method, we should prefer using its super-interface instead, to make the method more flexible.
Even though autoboxing/unboxing works “automagically” at compile-time, the added overhead in memory footprint and possible performance implications can offset the initial goal of more concise and performant code.
Just like the primitive wrappers, Java provides multiple specialized functional interfaces for primitive types, too.
The numeric primitives
double have their own specialized functional interfaces. Here are the ones for
BiConsumer-like interface, accepting an object-type and an
intargument. It’s supposed to have side-effects by affecting the consumed object argument.
Accepts an object-type argument and produces an
Accepts two object-type argument and produces an
All these interfaces exist for the other two types
double with the corresponding type names. The fact that some of the primitive types are missing isn’t too bad, they can easily be replaced by the existing types through casting.
The previous functional interfaces are either accepting or returning primitive types. But sometimes we want to accept and return a primitive. Java’s got you covered in this case, too:
boolean primitive doesn’t get as much love as the numeric primitive types. Only a single functional interface is explicitly prefixed:
Predicate<T> and its primitive counterparts can be seen as specialized functional interfaces returning
Default Methods on Functional Interfaces
For creating quite complex expressions, or simplifying lambda creation, many functional interfaces provided by the JDK have default methods. They are often designed as fluent interfaces.
Here are two examples, check out the functional interfaces mentioned in this article to find more.
A simple functional interface for comparing two objects of the same type. Let’s look at a simple example:
Thanks to the default method
Comparator.comparing(Function<T,U>) we can simplify the code:
Way simpler, and it can be nicely inlined into
Stream#sorted(Comparator<? super T> comparator).
Another functional interface with default methods is the already mentioned
Predicate<T>. It provides everything, the fundamental building blocks for building a multi-criteria predicate chain:
The JDK provides us with a lot of different functional interfaces. Due to Java’s two-fold type system differentiating between object-types and primitives, there’s a need for specialized interfaces for primitives if we want to handle them efficiently.
The fluent interfaces of many functional interfaces provide many parts to build complex expressions. Especially in combination with the Stream API and method references, we can craft quite concise and readable code.