Functional programming for Java developers, Part 2

Rewrite object-oriented code using functional techniques. Get started with lambdas, method references, functional interfaces, and the Streams API in Java

apples oranges slices mixture puzzle balance opposites fruit  savatore gersace flickr
Salvatore Gerace (CC BY 2.0)

Welcome back to this two-part tutorial introducing functional programming in a Java context. In Functional programming for Java developers, Part 1, I used JavaScript examples to get you started with five functional programming techniques: pure functions, higher-order functions, lazy evaluation, closures, and currying. Presenting those examples in JavaScript allowed us to focus on the techniques in a simpler syntax, without getting into Java's more complex functional programming capabilities.

In Part 2 we'll revisit those techniques using Java code that pre-dates Java 8. As you'll see, this code is functional, but it's not easy to write or read. You'll also be introduced to the new functional programming features that were fully integrated into the Java language in Java 8; namely, lambdas, method references, functional interfaces, and the Streams API.

Throughout this tutorial we'll revisit examples from Part 1 to see how the JavaScript and Java examples compare. You'll also see what happens when I update some of the pre-Java 8 examples with functional language features like lambdas and method references. Finally, this tutorial includes a hands-on exercise designed to help you practice functional thinking, which you'll do by transforming a piece of object-oriented Java code into its functional equivalent.

download
Download the source code for example applications in this tutorial. Created by Jeff Friesen for JavaWorld.

Functional programming with Java

Many developers don't realize it, but it was possible to write functional programs in Java before Java 8. In order to have a well-rounded view of functional programming in Java, let's quickly review functional programming features that predate Java 8. Once you've got those down, you'll likely have more appreciation for how new features introduced in Java 8 (like lambdas and functional interfaces) have simplified Java's approach to functional programming.

Functional programming before Java 8

Anonymous inner classes along with interfaces and closures are three older features that support functional programming in older versions of Java:

  • Anonymous inner classes let you pass functionality (described by interfaces) to methods.
  • Functional interfaces are interfaces that describe a function.
  • Closures let you access variables in their outer scopes.

In the sections that follow we'll revisit the five techniques introduced in Part 1, but using Java syntax. You'll see how each of these functional techniques was possible prior to Java 8.

Writing pure functions in Java

Listing 1 presents the source code to an example application, DaysInMonth, that is written using an an anonymous inner class and a functional interface. This application demonstrates how to write a pure function, which was achievable in Java long before Java 8.

Listing 1. A pure function in Java (DaysInMonth.java)

interface Function<T, R>
{
   R apply(T t);
}

public class DaysInMonth
{
   public static void main(String[] args)
   {
      Function<Integer, Integer> dim = new Function<Integer, Integer>()
      {
         @Override
         public Integer apply(Integer month)
         {
            return new Integer[] { 31, 28, 31, 30, 31, 30,
                                   31, 31, 30, 31, 30, 31 }[month];
         }
      };
      System.out.printf("April: %d%n", dim.apply(3));
      System.out.printf("August: %d%n", dim.apply(7));
   }
}

The generic Function interface in Listing 1 describes a function with a single parameter of type T and a return type of type R. The Function interface declares an R apply(T t) method that applies this function to the given argument.

The main() method instantiates an anonymous inner class that implements the Function interface. The apply() method unboxes month and uses it to index an array of days-in-month integers. The integer at this index is returned. (I'm ignoring leap years for simplicity.)

main() next executes this function twice by invoking apply() to return the day counts for the months of April and August. These counts are subsequently printed.

We've managed to create a function, and a pure function at that! Recall that a pure function depends only on its arguments and no external state. There are no side effects.

Compile Listing 1 as follows:

javac DaysInMonth.java

Run the resulting application as follows:

java DaysInMonth

You should observe the following output:

April: 30
August: 31

Writing higher-order functions in Java

Next, we'll look at higher-order functions, also known as first-class functions. Remember that a higher-order function receives function arguments and/or returns a function result. Java associates a function with a method, which is defined in an anonymous inner class. An instance of this class is passed to or returned from another Java method that serves as the higher-order function. The following file-oriented code fragment demonstrates passing a function to a higher-order function:

File[] txtFiles = 
   new File(".").listFiles(new FileFilter() 
                           {
                              @Override
                              public boolean accept(File pathname) 
                              {
                                 return pathname.getAbsolutePath().endsWith("txt");
                              }
                           });

This code fragment passes a function based on the java.io.FileFilter functional interface to the java.io.File class's File[] listFiles(FileFilter filter) method, telling it to return only those files with txt extensions.

Listing 2 shows another way to work with higher-order functions in Java. In this case, the code passes a comparator function to a sort() higher-order function for an ascending-order sort, and a second comparator function to sort() for a descending-order sort.

Listing 2. A higher-order function in Java (Sort.java)

import java.util.Comparator;

public class Sort
{
   public static void main(String[] args)
   {
      String[] innerplanets = { "Mercury", "Venus", "Earth", "Mars" };
      dump(innerplanets);
      sort(innerplanets, new Comparator<String>()
                         {
                            @Override
                            public int compare(String e1, String e2)
                            {
                               return e1.compareTo(e2);
                            }
                         });
      dump(innerplanets);
      sort(innerplanets, new Comparator<String>()
                         {
                            @Override
                            public int compare(String e1, String e2)
                            {
                               return e2.compareTo(e1);
                            }
                         });
      dump(innerplanets);
   }

   static <T> void dump(T[] array)
   {
      for (T element: array)
         System.out.println(element);
      System.out.println();
   }

   static <T> void sort(T[] array, Comparator<T> cmp)
   {
      for (int pass = 0; pass < array.length - 1; pass++)
         for (int i = array.length - 1; i > pass; i--)
            if (cmp.compare(array[i], array[pass]) < 0)
               swap(array, i, pass);
   }

   static <T> void swap(T[] array, int i, int j)
   {
      T temp = array[i];
      array[i] = array[j];
      array[j] = temp;
   }
}

Listing 2 imports the java.util.Comparator functional interface, which describes a function that can perform a comparison on two objects of arbitrary but identical type.

Two significant parts of this code are the sort() method (which implements the Bubble Sort algorithm) and the sort() invocations in the main() method. Although sort() is far from being functional, it demonstrates a higher-order function that receives a function--the comparator--as an argument. It executes this function by invoking its compare() method. Two instances of this function are passed in two sort() calls in main().

Compile Listing 2 as follows:

javac Sort.java

Run the resulting application as follows:

java Sort

You should observe the following output:

Mercury
Venus
Earth
Mars

Earth
Mars
Mercury
Venus

Venus
Mercury
Mars
Earth

Lazy evaluation in Java

Lazy evaluation is another functional programming technique that is not new to Java 8. This technique delays the evaluation of an expression until its value is needed. In most cases, Java eagerly evaluates an expression that is bound to a variable. Java supports lazy evaluation for the following specific syntax:

  • The Boolean && and || operators, which will not evaluate their right operand when the left operand is false (&&) or true (||).
  • The ?: operator, which evaluates a Boolean expression and subsequently evaluates only one of two alternative expressions (of compatible type) based on the Boolean expression's true/false value.

Functional programming encourages expression-oriented programming, so you'll want to avoid using statements as much as possible. For example, suppose you want to replace Java's if-else statement with an ifThenElse() method. Listing 3 shows a first attempt.

Listing 3. An example of eager evaluation in Java (EagerEval.java)

public class EagerEval
{
   public static void main(String[] args)
   {
      System.out.printf("%d%n", ifThenElse(true, square(4), cube(4)));
      System.out.printf("%d%n", ifThenElse(false, square(4), cube(4)));
   }

   static int cube(int x)
   {
      System.out.println("in cube");
      return x * x * x;
   }

   static int ifThenElse(boolean predicate, int onTrue, int onFalse)
   {
      return (predicate) ? onTrue : onFalse;
   }

   static int square(int x)
   {
      System.out.println("in square");
      return x * x;
   }
}

Listing 3 defines an ifThenElse() method that takes a Boolean predicate and a pair of integers, returning the onTrue integer when the predicate is true and the onFalse integer otherwise.

Listing 3 also defines cube() and square() methods. Respectively, these methods cube and square an integer and return the result.

The main() method invokes ifThenElse(true, square(4), cube(4)), which should invoke only square(4), followed by ifThenElse(false, square(4), cube(4)), which should invoke only cube(4).

Compile Listing 3 as follows:

javac EagerEval.java

Run the resulting application as follows:

java EagerEval

You should observe the following output:

in square
in cube
16
in square
in cube
64

The output shows that each ifThenElse() call results in both methods executing, irrespective of the Boolean expression. We cannot leverage the ?: operator's laziness because Java eagerly evaluates the method's arguments.

Although there's no way to avoid eager evaluation of method arguments, we can still take advantage of ?:'s lazy evaluation to ensure that only square() or cube() is called. Listing 4 shows how.

Listing 4. An example of lazy evaluation in Java (LazyEval.java)

interface Function<T, R>
{
   R apply(T t);
}

public class LazyEval
{
   public static void main(String[] args)
   {
      Function<Integer, Integer> square = new Function<Integer, Integer>()
                                          {
                                             {
                                                System.out.println("SQUARE");
                                             }
                                             @Override
                                             public Integer apply(Integer t)
                                             {
                                                System.out.println("in square");
                                                return t * t;
                                             }
                                          };

      Function<Integer, Integer> cube = new Function<Integer, Integer>()
                                        {
                                           {
                                              System.out.println("CUBE");
                                           }

                                           @Override
                                           public Integer apply(Integer t)
                                           {
                                              System.out.println("in cube");
                                              return t * t * t;
                                           }
                                        };

      System.out.printf("%d%n", ifThenElse(true, square, cube, 4));
      System.out.printf("%d%n", ifThenElse(false, square, cube, 4));
   }

   static <T, R> R ifThenElse(boolean predicate, Function<T, R> onTrue,
                              Function<T, R> onFalse, T t)
   {
      return (predicate ? onTrue.apply(t) : onFalse.apply(t));
   }
}

Listing 4 turns ifThenElse() into a higher-order function by declaring this method to receive a pair of Function arguments. Although these arguments are eagerly evaluated when passed to ifThenElse(), the ?: operator causes only one of these functions to execute (via apply()). You can see both eager and lazy evaluation at work when you compile and run the application.

Compile Listing 4 as follows:

javac LazyEval.java

Run the resulting application as follows:

java LazyEval

You should observe the following output:

SQUARE
CUBE
in square
16
in cube
64

Closures in Java

An anonymous inner class instance is associated with a closure. Outer scope variables must be declared final or (starting in Java 8) effectively final (meaning unmodified after initialization) in order to be accessible. Consider Listing 5.

Listing 5. An example of closures in Java (PartialAdd.java)

interface Function<T, R>
{
   R apply(T t);
}

public class PartialAdd
{
   Function<Integer, Integer> add(final int x)
   {
      Function<Integer, Integer> partialAdd = new Function<Integer, Integer>()
                                              {
                                                 @Override
                                                 public Integer apply(Integer y)
                                                 {
                                                    return y + x;
                                                 }
                                              };
      return partialAdd;
   }

   public static void main(String[] args)
   {
      PartialAdd pa = new PartialAdd();
      Function<Integer, Integer> add10 = pa.add(10);
      Function<Integer, Integer> add20 = pa.add(20);

      System.out.println(add10.apply(5));
      System.out.println(add20.apply(5));
   }
}

Listing 5 is the Java equivalent of the closure I previously presented in JavaScript (see Part 1, Listing 8). This code declares an add() higher-order function that returns a function for performing partial application of the add() function. The apply() method accesses variable x in the outer scope of add(), which must be declared final prior to Java 8. The code behaves pretty much the same as the JavaScript equivalent.

Compile Listing 5 as follows:

javac PartialAdd.java

Run the resulting application as follows:

java PartialAdd

You should observe the following output:

15
25

Currying in Java

You might have noticed that the PartialAdd in Listing 5 demonstrates more than just closures. It also demonstrates currying, which is a way to translate a multi-argument function's evaluation into the evaluation of an equivalent sequence of single-argument functions. Both pa.add(10) and pa.add(20) in Listing 5 return a closure that records an operand (10 or 20, respectively) and a function that performs the addition--the second operand (5) is passed via add10.apply(5) or add20.apply(5).

Currying lets us evaluate function arguments one at a time, producing a new function with one less argument on each step. For example, in the PartialAdd application, we are currying the following function:

f(x, y) = x + y

We could apply both arguments at the same time, yielding the following:

f(10, 5) = 10 + 5

However, with currying, we apply only the first argument, yielding this:

f(10, y) = g(y) = 10 + y

We now have a single function, g, that takes only a single argument. This is the function that will be evaluated when we call the apply() method.

You might be confused by my use of the phrase "partial application," especially because I stated in Part 1 that currying isn't the same as partial application, which is the process of fixing a number of arguments to a function, producing another function of smaller arity. With partial application, you can produce functions with more than one argument, but with currying, each function must have exactly one argument.

Listing 5 presents a small example of Java-based currying prior to Java 8. Now consider the CurriedCalc application in Listing 6.

Listing 6. Currying in Java code (CurriedCalc.java)

interface Function<T, R>
{
   R apply(T t);
}

public class CurriedCalc
{
   public static void main(String[] args)
   {
      System.out.println(calc(1).apply(2).apply(3).apply(4));
   }

   static Function<Integer, Function<Integer, Function<Integer, Integer>>> 
      calc(final Integer a)
   {
      return new Function<Integer, 
                          Function<Integer, Function<Integer, Integer>>>()
             {
                @Override
                public Function<Integer, Function<Integer, Integer>> 
                   apply(final Integer b)
                {
                   return new Function<Integer, Function<Integer, Integer>>()
                          {
                             @Override
                             public Function<Integer, Integer> 
                                apply(final Integer c)
                             {
                                return new Function<Integer, Integer>()
                                {
                                   @Override
                                   public Integer apply(Integer d)
                                   {
                                      return (a + b) * (c + d);
                                   }
                                };
                             }
                          };
                }
             };
   }
}

Listing 6 uses currying to evaluate the function f(a, b, c, d) = (a + b) * (c + d). Given expression calc(1).apply(2).apply(3).apply(4), this function is curried as follows:

  1. f(1, b, c, d) = g(b, c, d) = (1 + b) * (c + d)
  2. g(2, c, d) = h(c, d) = (1 + 2) * (c + d)
  3. h(3, d) = i(d) = (1 + 2) * (3 + d)
  4. i(4) = (1 + 2) * (3 + 4)

Compile Listing 6:

javac CurriedCalc.java

Run the resulting application:

java CurriedCalc

You should observe the following output:

21

Because currying is about performing partial application of a function, it doesn't matter in what order the arguments are applied. For example, instead of passing a to calc() and d to the most-nested apply() method (which performs the calculation), we could reverse these parameter names. This would result in d c b a instead of a b c d, but it would still achieve the same result of 21. (The source code for this tutorial includes the alternative version of CurriedCalc.)

Functional programming in Java 8

Functional programming before Java 8 isn't pretty. Too much code is required to create, pass a function to, and/or return a function from a first-class function. Prior versions of Java also lack predefined functional interfaces and first-class functions such as filter and map.

Java 8 reduces verbosity largely by introducing lambdas and method references to the Java language. It also offers predefined functional interfaces, and it makes filter, map, reduce, and other reusable first-class functions available via the Streams API.

We'll look at these improvements together in the next sections.

Writing lambdas in Java code

A lambda is an expression that describes a function by denoting an implementation of a functional interface. Here's an example:

() -> System.out.println("my first lambda")

From left to right, () identifies the lambda's formal parameter list (there are no parameters), -> signifies a lambda expression, and System.out.println("my first lambda") is the lambda's body (the code to be executed).

A lambda has a type, which is any functional interface for which the lambda is an implementation. One such type is java.lang.Runnable, because Runnable's void run() method also has an empty formal parameter list:

Runnable r = () -> System.out.println("my first lambda");

You can pass the lambda anywhere that a Runnable argument is required; for example, the Thread(Runnable r) constructor. Assuming that the previous assignment has occurred, you could pass r to this constructor, as follows:

new Thread(r);

Alternatively, you could pass the lambda directly to the constructor:

new Thread(() -> System.out.println("my first lambda"));

This is definitely more compact than the pre-Java 8 version:

new Thread(new Runnable()
           {
              @Override
              public void run()
              {
                 System.out.println("my first lambda");
              }
           });

A lambda-based file filter

My previous demonstration of higher-order functions presented a file filter based on an anonymous inner class. Here's the lambda-based equivalent:

File[] txtFiles = new File(".").listFiles(p -> p.getAbsolutePath().endsWith("txt"));

Return statements in lambda expressions

In Part 1, I mentioned that functional programming languages work with expressions as opposed to statements. Prior to Java 8, you could largely eliminate statements in functional programming, but you couldn't eliminate the return statement.

The above code fragment shows that a lambda doesn't require a return statement to return a value (a Boolean true/false value, in this case): you just specify the expression without return [and add] a semicolon. However, for multi-statement lambdas, you'll still need the return statement. In these cases you must place the lambda's body between braces as follows (don't forget the semicolon to terminate the statement):

File[] txtFiles = new File(".").listFiles(p -> { return p.getAbsolutePath().endsWith("txt"); });

Lambdas with functional interfaces

I have two more examples to illustrate the conciseness of lambdas. First, let's revisit the main() method from the Sort application shown in Listing 2:

public static void main(String[] args)
{
   String[] innerplanets = { "Mercury", "Venus", "Earth", "Mars" };
   dump(innerplanets);
   sort(innerplanets, (e1, e2) -> e1.compareTo(e2));
   dump(innerplanets);
   sort(innerplanets, (e1, e2) -> e2.compareTo(e1));
   dump(innerplanets);
}

We can also update the calc() method from the CurriedCalc application shown in Listing 6:

static Function<Integer, Function<Integer, Function<Integer, Integer>>> 
   calc(Integer a)
{
   return b -> c -> d -> (a + b) * (c + d);
}

Runnable, FileFilter, and Comparator are examples of functional interfaces, which describe functions. Java 8 formalized this concept by requiring a functional interface to be annotated with the java.lang.FunctionalInterface annotation type, as in @FunctionalInterface. An interface that is annotated with this type must declare exactly one abstract method.

You can use Java's pre-defined functional interfaces (discussed later), or you can easily specify your own, as follows:

@FunctionalInterface
interface Function<T, R>
{
   R apply(T t);
}

You might then use this functional interface as shown here:

public static void main(String[] args)
{
   System.out.println(getValue(t -> (int) (Math.random() * t), 10));
   System.out.println(getValue(x -> x * x, 20));
}

static Integer getValue(Function<Integer, Integer> f, int x)
{
   return f.apply(x);
}

Method references in Java

Some lambdas only invoke an existing method. For example, the following lambda invokes System.out's void println(s) method on the lambda's single argument:

(String s) -> System.out.println(s)

The lambda presents (String s) as its formal parameter list and a code body whose System.out.println(s) expression prints s's value to the standard output stream.

To save keystrokes, you could replace the lambda with a method reference, which is a compact reference to an existing method. For example, you could replace the previous code fragment with the following:

System.out::println

Here, :: signifies that System.out's void println(String s) method is being referenced. The method reference results in much shorter code than we achieved with the previous lambda.

A method reference for Sort

I previously showed a lambda version of the Sort application from Listing 2. Here is that same code written with a method reference instead:

public static void main(String[] args)
{
   String[] innerplanets = { "Mercury", "Venus", "Earth", "Mars" };
   dump(innerplanets);
   sort(innerplanets, String::compareTo);
   dump(innerplanets);
   sort(innerplanets, Comparator.comparing(String::toString).reversed());
   dump(innerplanets);
}

The String::compareTo method reference version is shorter than the lambda version of (e1, e2) -> e1.compareTo(e2). Note, however, that a longer expression is required to create an equivalent reverse-order sort, which also includes a method reference: String::toString. Instead of specifying String::toString, I could have specified the equivalent s -> s.toString() lambda.

Predefined functional interfaces

Java 8 introduced predefined functional interfaces (java.util.function) so that developers don't have create our own functional interfaces for common tasks. Here are a few examples:

  • The Consumer<T> functional interface represents an operation that accepts a single input argument and returns no result. Its void accept(T t) method performs this operation on argument t.
  • The Function<T, R> functional interface represents a function that accepts one argument and returns a result. Its R apply(T t) method applies this function to argument t and returns the result.
  • The Predicate<T> functional interface represents a predicate (Boolean-valued function) of one argument. Its boolean test(T t) method evaluates this predicate on argument t and returns true or false.
  • The Supplier<T> functional interface represents a supplier of results. Its T get() method receives no argument(s) but returns a result.

The DaysInMonth application in Listing 1 revealed a complete Function interface. Starting with Java 8, you can remove this interface and import the identical predefined Function interface.

Functional APIs: Streams

Java 8 introduced the Streams API to facilitate sequential and parallel processing of data items. This API is based on streams, where a stream is a sequence of elements originating from a source and supporting sequential and parallel aggregate operations. A source stores elements (such as a collection) or generates elements (such as a random number generator). An aggregate is a result calculated from multiple input values.

A stream supports intermediate and terminal operations. An intermediate operation returns a new stream, whereas a terminal operation consumes the stream. Operations are connected into a pipeline (via method chaining). The pipeline starts with a source, which is followed by zero or more intermediate operations, and ends with a terminal operation.

Streams is an example of a functional API. It offers filter, map, reduce, and other reusable first-class functions. I briefly demonstrated this API in the Employees application shown in Part 1, Listing 1. Listing 7 offers another example.

Listing 7. Functional programming with Streams (StreamFP.java)

import java.util.Random;

import java.util.stream.IntStream;

public class StreamFP
{
   public static void main(String[] args)
   {
      new Random().ints(0, 11).limit(10).filter(x -> x % 2 == 0)
                  .forEach(System.out::println);
      System.out.println();

      String[] cities = 
      {
         "New York",
         "London",
         "Paris",
         "Berlin",
         "BrasÌlia",
         "Tokyo",
         "Beijing",
         "Jerusalem",
         "Cairo",
         "Riyadh",
         "Moscow"
      };
      IntStream.range(0, 11).mapToObj(i -> cities[i])
               .forEach(System.out::println);
      System.out.println();

      System.out.println(IntStream.range(0, 10).reduce(0, (x, y) -> x + y));
      System.out.println(IntStream.range(0, 10).reduce(0, Integer::sum));
   }
}

The main() method first creates a stream of pseudorandom integers starting at 0 and ending at 10. The stream is limited to exactly 10 integers. The filter() first-class function receives a lambda as its predicate argument. The predicate removes odd integers from the stream. Finally, the forEach() first-class function prints each even integer to the standard output via the System.out::println method reference.

The main() method next creates an integer stream that produces a sequential range of integers starting at 0 and ending at 10. The mapToObj() first-class function receives a lambda that maps an integer to the equivalent string at the integer index in the cities array. The city name is then sent to the standard output via the forEach() first-class function and its System.out::println method reference.

Lastly, main() demonstrates the reduce() first-class function. An integer stream that produces the same range of integers as in the previous example is reduced to a sum of their values, which is subsequently output.

Compile Listing 7 as follows:

javac StreamFP.java

Run the resulting application as follows:

java StreamFP

I observed the following output from one run:

0
2
10
6
0
8
10

New York
London
Paris
Berlin
BrasÌlia
Tokyo
Beijing
Jerusalem
Cairo
Riyadh
Moscow

45
45

You might have expected 10 instead of 7 pseudorandom even integers (ranging from 0 through 10, thanks to range(0, 11)) to appear at the beginning of the output. After all, limit(10) seems to indicate that 10 integers will be output. However, this isn't the case. Although the limit(10) call results in a stream of exactly 10 integers, the filter(x -> x % 2 == 0) call results in odd integers being removed from the stream.

In conclusion

Many Java developers won't pursue pure functional programming in a language like Haskell because it differs so greatly from the familiar imperative, object-oriented paradigm. Java 8's functional programming capabilities are designed to bridge that gap, enabling Java developers to write code that's easier to understand, maintain, and test. Functional code is also more reusable and more suitable for parallel processing in Java. With all of these incentives, there's really no reason not to incorporate Java's functional programming options into your Java code.

This story, "Functional programming for Java developers, Part 2" was originally published by JavaWorld.

Copyright © 2018 IDG Communications, Inc.

InfoWorld Technology of the Year Awards 2023. Now open for entries!