Functional Programming With Java: What’s in the Box

 · 4 min
Erda Estremera on Unsplash

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.

The package 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:

  • T Supplier<T>#get()
    Takes no arguments, but returns an object. A method reference to a simple POJO-Getter qualifies as a Supplier.

  • void Consumer<T>#accept(T t)
    Takes a single argument and doesn’t return anything. Every POJO-Setter qualifies as a Consumer.

  • 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 boolean primitive.

With just these four alone we can do a lot of functional programming:

java
Supplier<List<String>> lazy =
    () -> Arrays.asList("apples",
                        "oranges",
                        "pear",
                        "ananas",
                        "banana");

lazy.get()
    .stream()
    // Predicate<String> 
    .filter(in -> in.startsWith("a"))
    // Function<String, String>
    .map(String::toUpperCase)
    // Consumer<String>
    .forEach(System.out::println);

// Output:
// APPLES
// ANANAS

Function Arity

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.

Primitive Types

Until Project Valhalla and generic specialization (JEP-218) will be available, we can’t use primitives as parameterized types.

Even though autoboxing 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.

Numeric primitives

The numeric primitives int, long, and double have their own specialized functional interfaces. Here are the ones for int:

All these interfaces exist for the other two types long and 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.

Conversion functions

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

The boolean primitive doesn’t get as much love as the numeric primitive types. Only a single functional interface is explicitly prefixed:

But Predicate<T> and its primitive counterparts can be seen as specialized functional interfaces returning boolean primitives.

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.

Comparator

A simple functional interface for comparing two objects of the same type. Let’s look at a simple example:

java
Comparator<MyBean> lambda =
    (lhs, rhs) -> lhs.getProperty().compareTo(rhs.getProperty());

Thanks to the default method Comparator.comparing(Function<T,U>) we can simplify the code:

java
Comparator<MyBean> lambda =
    Comparator.comparing(MyBean::getProperty);

Way simpler, and it can be nicely inlined into Stream#sorted(Comparator<? super T> comparator).

Predicate

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:

java
Predicate<String> filter1 = str -> str.startsWith("a");
Predicate<String> filter2 = str -> str.startsWith("b");
Predicate<String> filter3 = str -> str.contains("n");

// Starting with "a" or "b", but not containing "n"
Predicate<String> combined = filter1.or(filter2)
                                    .and(filter3.negate());

Arrays.asList("apples",
              "oranges",
              "avocados",
              "pears",
              "ananas",
              "bananas")
      .stream()
      .filter(combined)
      .forEach(System.out::println);

// OUTPUT:
// apples
// avocados

Conclusion

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.


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!
Available in English, Polish, Korean, and soon, Chinese.

Resources