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

Welcome back to this two-part tutorial introducing functional programming in a Java context. In 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 sidebar designed to help you practice functional thinking, which you'll do by transforming a piece of object-oriented Java code into its functional equivalent.

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));
   }
}
1 2 3 Page 1
Page 1 of 3