- Learning Java Functional Programming
- Richard M.Reese
- 4118字
- 2021-07-09 21:44:14
Functional programming concepts in Java
In this section, we will examine the underlying concept of functions and how they are implemented in Java 8. This includes high-order, first-class, and pure functions.
A first-class function is a function that can be used where other first-class entities can be used. These types of entities include primitive data types and objects. Typically, they can be passed to and returned from functions and methods. In addition, they can be assigned to variables.
A high-order function either takes another function as an argument or returns a function as the return value. Languages that support this type of function are more flexible. They allow a more natural flow and composition of operations. The use of composition is explored in Chapter 3, Function Composition and Fluent Interfaces.
Pure functions have no side effects. The function does not modify nonlocal variables and does not perform I/O.
High-order functions
We will demonstrate the creation and use of the high-order function using an imperative and a functional approach to convert letters of a string to lowercase. The next code sequence reuses the list
variable, developed in the previous section, to illustrate the imperative approach. The for-each statement iterates through each element of the list using the String
class' toLowerCase
method to perform the conversion:
for(String element : list) { System.out.println(element.toLowerCase()); }
The output of this sequence will display each name in the list, in lowercase and on a separate line.
To demonstrate the use of a high-order function, we will create a function called processString
, which is passed a function as the first parameter and then apply this function to the second parameter as shown next:
public String processString(Function<String,String> operation,String target) { return operation.apply(target); }
The function passed will be an instance of the java.util.function
package's Function
interface. This interface possesses an accept
method that passes one data type and returns a potentially different data type. With our definition, it is passed String
and returns String
.
In the next code sequence, a lambda expression using the toLowerCase
method is passed to the processString
method. As you may remember, the forEach
method accepts a lambda expression, which matches the Consumer
interface's accept
method. The lambda expression passed to the processString
method matches the Function
interface's accept
method. The output is the same as produced by the equivalent imperative implementation.
list.forEach(s ->System.out.println( processString(t->t.toLowerCase(), s)));
We could have also used a method reference as show next:
list.forEach(s ->System.out.println( processString(String::toLowerCase, s)));
The use of the high-order function may initially seem to be a bit convoluted. We needed to create the processString
function and then pass either a lambda expression or a method reference to perform the conversion. While this is true, the benefit of this approach is flexibility. If we needed to perform a different string operation other than converting the target string to lowercase, we will need to essentially duplicate the imperative code and replace toLowerCase
with a new method such as toUpperCase
. However, with the functional approach, all we need to do is replace the method used as shown next:
list.forEach(s ->System.out.println(processString( t->t.toUpperCase(), s)));
This is simpler and more flexible. A lambda expression can also be passed to another lambda expression.
Let's consider another example where high-order functions can be useful. Suppose we need to convert a list of one type into a list of a different type. We might have a list of strings that we wish to convert to their integer equivalents. We might want to perform a simple conversion or perhaps we might want to double the integer value. We will use the following lists:
List<String> numberString = Arrays.asList("12", "34", "82"); List<Integer> numbers = new ArrayList<>(); List<Integer> doubleNumbers = new ArrayList<>();
The following code sequence uses an iterative approach to convert the string list into an integer list:
for (String num : numberString) { numbers.add(Integer.parseInt(num)); }
The next sequence uses a stream to perform the same conversion:
numbers.clear(); numberString .stream() .forEach(s -> numbers.add(Integer.parseInt(s)));
There is not a lot of difference between these two approaches, at least from a number of lines perspective. However, the iterative solution will only work for the two lists: numberString
and numbers
. To avoid this, we could have written the conversion routine as a method.
We could also use lambda expression to perform the same conversion. The following two lambda expression will convert a string list to an integer list and from a string list to an integer list where the integer has been doubled:
Function<List<String>, List<Integer>> singleFunction = s -> { s.stream() .forEach(t -> numbers.add(Integer.parseInt(t))); return numbers; }; Function<List<String>, List<Integer>> doubleFunction = s -> { s.stream() .forEach(t -> doubleNumbers.add( Integer.parseInt(t) * 2)); return doubleNumbers; };
We can apply these two functions as shown here:
numbers.clear(); System.out.println(singleFunction.apply(numberString)); System.out.println(doubleFunction.apply(numberString));
The output follows:
[12, 34, 82] [24, 68, 164]
However, the real power comes from passing these functions to other functions. In the next code sequence, a stream is created consisting of a single element, a list. This list contains a single element, the numberString
list. The map
method expects a Function
interface instance. Here, we use the doubleFunction
function. The list of strings is converted to integers and then doubled. The resulting list is displayed:
Arrays.asList(numberString).stream() .map(doubleFunction) .forEach(s -> System.out.println(s));
The output follows:
[24, 68, 164]
We passed a function to a method. We could easily pass other functions to achieve different outputs.
Returning a function
When a value is returned from a function or method, it is intended to be used elsewhere in the application. Sometimes, the return value is used to determine how subsequent computations should proceed. To illustrate how returning a function can be useful, let's consider a problem where we need to calculate the pay of an employee based on the numbers of hours worked, the pay rate, and the employee type.
To facilitate the example, start with an enumeration representing the employee type:
enum EmployeeType {Hourly, Salary, Sales};
The next method illustrates one way of calculating the pay using an imperative approach. A more complex set of computation could be used, but these will suffice for our needs:
public float calculatePay(int hourssWorked, float payRate, EmployeeType type) { switch (type) { case Hourly: return hourssWorked * payRate; case Salary: return 40 * payRate; case Sales: return 500.0f + 0.15f * payRate; default: return 0.0f; } }
If we assume a 7 day workweek, then the next code sequence shows an imperative way of calculating the total number of hours worked:
int hoursWorked[] = {8, 12, 8, 6, 6, 5, 6, 0}; int totalHoursWorked = 0; for (int hour : hoursWorked) { totalHoursWorked += hour; }
Alternatively, we could have used a stream to perform the same operation as shown next. The Arrays
class's stream
method accepts an array of integers and converts it into a Stream
object. The sum
method is applied fluently, returning the number of hours worked:
totalHoursWorked = Arrays.stream(hoursWorked).sum();
The latter approach is simpler and easier to read. To calculate and display the pay, we can use the following statement which, when executed, will return 803.25
.
System.out.println( calculatePay(totalHoursWorked, 15.75f, EmployeeType.Hourly));
The functional approach is shown next. A calculatePayFunction
method is created that is passed the employee type and returns a lambda expression. This will compute the pay based on the number of hours worked and the pay rate. This lambda expression is based on the BiFunction
interface. It has an accept
method that takes two arguments and returns a value. Each of the parameters and the return type can be of different data types. It is similar to the Function
interface's accept
method, except that it is passed two arguments instead of one.
The calculatePayFunction
method is shown next. It is similar to the imperative's calculatePay
method, but returns a lambda expression:
public BiFunction<Integer, Float, Float> calculatePayFunction( EmployeeType type) { switch (type) { case Hourly: return (hours, payRate) -> hours * payRate; case Salary: return (hours, payRate) -> 40 * payRate; case Sales: return (hours, payRate) -> 500f + 0.15f * payRate; default: return null; } }
It can be invoked as shown next:
System.out.println( calculatePayFunction(EmployeeType.Hourly) .apply(totalHoursWorked, 15.75f));
When executed, it will produce the same output as the imperative solution. The advantage of this approach is that the lambda expression can be passed around and executed in different contexts. In addition, it can be combined with other functions in more powerful ways as we will see in Chapter 3, Function Composition and Fluent Interfaces .
First-class functions
To demonstrate first-class functions, we use lambda expressions. Assigning a lambda expression, or method reference, to a variable can be done in Java 8. Simply declare a variable of the appropriate function type and use the assignment operator to do the assignment.
In the following statement, a reference variable to the previously defined BiFunction
-based lambda expression is declared along with the number of hours worked:
BiFunction<Integer, Float, Float> calculateFunction; int hoursWorked = 51;
We can easily assign a lambda expression to this variable. Here, we use the lambda expression returned from the calculatePayFunction
method:
calculateFunction = calculatePayFunction(EmployeeType.Hourly);
The reference variable can then be used as shown in this statement:
System.out.println( calculateFunction.apply(hoursWorked, 15.75f));
It produces the same output as before.
One shortcoming of the way an hourly employee's pay is computed is that overtime pay is not handled. We can add this functionality to the calculatePayFunction
method. However, to further illustrate the use of reference variables, we will assign one of two lambda expressions to the calculateFunction
variable based on the number of hours worked as shown here:
if(hoursWorked<=40) { calculateFunction = (hours, payRate) -> 40 * payRate; } else { calculateFunction = (hours, payRate) -> hours*payRate + (hours-40)*1.5f*payRate; }
When the expression is evaluated as shown next, it returns a value of 1063.125
:
System.out.println( calculateFunction.apply(hoursWorked, 15.75f));
Let's rework the example developed in the High-order functions section, where we used lambda expressions to display the lowercase values of an array of string. Part of the code has been duplicated here for your convenience:
list.forEach(s ->System.out.println( processString(t->t.toLowerCase(), s)));
Instead, we will use variables to hold the lambda expressions for the Consumer
and Function
interfaces as shown here:
Consumer<String> consumer; consumer = s -> System.out.println(toLowerFunction.apply(s)); Function<String,String> toLowerFunction; toLowerFunction= t -> t.toLowerCase();
The declaration and initialization could have been done with one statement for each variable. To display all of the names, we simply use the consumer
variable as the argument of the forEach
method:
list.forEach(consumer);
This will display the names as before. However, this is much easier to read and follow. The ability to use lambda expressions as first-class entities makes this possible.
We can also assign method references to variables. Here, we replaced the initialization of the function
variable with a method reference:
function = String::toLowerCase;
The output of the code will not change.
The pure function
The pure function is a function that has no side effects. By side effects, we mean that the function does not modify nonlocal variables and does not perform I/O. A method that squares a number is an example of a pure method with no side effects as shown here:
public class SimpleMath { public static int square(int x) { return x * x; } }
Its use is shown here and will display the result, 25
:
System.out.println(SimpleMath.square(5));
An equivalent lambda expression is shown here:
Function<Integer,Integer> squareFunction = x -> x*x; System.out.println(squareFunction.apply(5));
The advantages of pure functions include the following:
We will examine each of these advantages in more depth.
Using the same arguments will produce the same results. The previous square operation is an example of this. Since the operation does not depend on other external values, re-executing the code with the same arguments will return the same results.
This supports the optimization technique call memoization. This is the process of caching the results of an expensive execution sequence and retrieving them when they are used again.
An imperative technique for implementing this approach involves using a hash map to store values that have already been computed and retrieving them when they are used again. Let's demonstrate this using the square
function. The technique should be used for those functions that are compute intensive. However, using the square
function will allow us to focus on the technique.
Declare a cache to hold the previously computed values as shown here:
private final Map<Integer, Integer> memoizationCache = new HashMap<>();
We need to declare two methods. The first method, called doComputeExpensiveSquare
, does the actual computation as shown here. A display statement is included only to verify the correct operation of the technique. Otherwise, it is not needed. The method should only be called once for each unique value passed to it.
private Integer doComputeExpensiveSquare(Integer input) { System.out.println("Computing square"); return 2 * input; }
A second method is used to detect when a value is used a subsequent time and return the previously computed value instead of calling the square
method. This is shown next. The containsKey
method checks to see if the input value has already been used. If it hasn't, then the doComputeExpensiveSquare
method is called. Otherwise, the cached value is returned.
public Integer computeExpensiveSquare(Integer input) { if (!memoizationCache.containsKey(input)) { memoizationCache.put(input, doComputeExpensiveSquare(input)); } return memoizationCache.get(input); }
The use of the technique is demonstrated with the next code sequence:
System.out.println(computeExpensiveSquare(4)); System.out.println(computeExpensiveSquare(4));
The output follows, which demonstrates that the square
method was only called once:
Computing square 16 16
The problem with this approach is the declaration of a hash map. This object may be inadvertently used by other elements of the program and will require the explicit declaration of new hash maps for each memoization usage. In addition, it does not offer flexibility in handling multiple memoization. A better approach is available in Java 8. This new approach wraps the hash map in a class and allows easier creation and use of memoization.
Let's examine a memoization class as adapted from http://java.dzone.com/articles/java-8-automatic-memoization. It is called Memoizer. It uses ConcurrentHashMap
to cache value and supports concurrent access from multiple threads.
Two methods are defined. The doMemoize
method returns a lambda expression that does all of the work. The memorize
method creates an instance of the Memoizer
class and passes the lambda expression implementing the expensive operation to the doMemoize
method.
The doMemoize
method uses the ConcurrentHashMap
class's computeIfAbsent
method to determine if the computation has already been performed. If the value has not been computed, it executes the Function
interface's apply
method against the function argument:
public class Memoizer<T, U> { private final Map<T, U> memoizationCache = new ConcurrentHashMap<>(); private Function<T, U> doMemoize(final Function<T, U> function) { return input -> memoizationCache.computeIfAbsent(input, function::apply); } public static <T, U> Function<T, U> memoize(final Function<T, U> function) { return new Memoizer<T, U>().doMemoize(function); } }
A lambda expression is created for the square operation:
Function<Integer, Integer> squareFunction = x -> { System.out.println("In function"); return x * x; };
The memoizationFunction
variable will hold the lambda expression that is subsequently used to invoke the square operations:
Function<Integer, Integer> memoizationFunction = Memoizer.memoize(squareFunction); System.out.println(memoizationFunction.apply(2)); System.out.println(memoizationFunction.apply(2)); System.out.println(memoizationFunction.apply(2));
The output of this sequence follows where the square operation is performed only once:
In function 4 4 4
We can easily use the Memoizer
class for a different function as shown here:
Function<Double, Double> memoizationFunction2 = Memoizer.memoize(x -> x * x); System.out.println(memoizationFunction2.apply(4.0));
This will square the number as expected. Functions that are recursive present additional problems. Recursion will be addressed in Chapter 5, Recursion Techniques in Java 8.
When dependencies between functions are eliminated, then more flexibility in the order of execution is possible. Consider these Function
and BiFunction
declarations, which define simple expressions for computing hourly, salaried, and sales type pay, respectively:
BiFunction<Integer, Double, Double> computeHourly = (hours, rate) -> hours * rate; Function<Double, Double> computeSalary = rate -> rate * 40.0; BiFunction<Double, Double, Double> computeSales = (rate, commission) -> rate * 40.0 + commission;
These functions can be executed, and their results are assigned to variables as shown here:
double hourlyPay = computeHourly.apply(35, 12.75); double salaryPay = computeSalary.apply(25.35); double salesPay = computeSales.apply(8.75, 2500.0);
These are pure functions as they do not use external values to perform their computations. In the following code sequence, the sum of all three pays are totaled and displayed:
System.out.println(computeHourly.apply(35, 12.75) + computeSalary.apply(25.35) + computeSales.apply(8.75, 2500.0));
We can easily reorder their execution sequence or even execute them concurrently, and the results will be the same. There are no dependencies between the functions that restrict them to a specific execution ordering.
Continuing with this example, let's add an additional sequence, which computes the total pay based on the type of employee. The variable, hourly
, is set to true
if we want to know the total of the hourly employee pay type. It will be set to false
if we are interested in salary and sales-type employees:
double total = 0.0; boolean hourly = ...; if(hourly) { total = hourlyPay; } else { total = salaryPay + salesPay; } System.out.println(total);
When this code sequence is executed with an hourly value of false
, there is no need to execute the computeHourly
function since it is not used. The runtime system could conceivably choose not to execute any of the lambda expressions until it knows which one is actually used.
While all three functions are actually executed in this example, it illustrates the potential for lazy evaluation. Functions are not executed until needed. Lazy evaluation does occur with streams as we will demonstrate in Chapter 4, Streams and the Evaluation of Expressions.
Referential transparency
Referential transparency is the idea that a given expression is made up of subexpressions. The value of the subexpression is important. We are not concerned about how it is written or other details. We can replace the subexpression with its value and be perfectly happy.
With regards to pure functions, they are said to be referentially transparent since they have same effect. In the next declaration, we declare a pure function called pureFunction
:
Function<Double,Double> pureFunction = t -> 3*t;
It supports referential transparency. Consider if we declare a variable as shown here:
int num = 5;
Later, in a method we can assign a different value to the variable:
num = 6;
If we define a lambda expression that uses this variable, the function is no longer pure:
Function<Double,Double> impureFunction = t -> 3*t+num;
The function no longer supports referential transparency.
Closure in Java
The use of external variables in a lambda expression raises several interesting questions. One of these involves the concept of closures. A closure is a function that uses the context within which it was defined. By context, we mean the variables within its scope. This sometimes is referred to as variable capture.
We will use a class called ClosureExample
to illustrate closures in Java. The class possesses a getStringOperation
method that returns a Function
lambda expression. This expression takes a string argument and returns an augmented version of it. The argument is converted to lowercase, and then its length is appended to it twice. In the process, both an instance variable and a local variable are used.
In the implementation that follows, the instance variable and two local variables are used. One local variable is a member of the getStringOperation
method and the second one is a member of the lambda expression. They are used to hold the length of the target string and for a separator string:
public class ClosureExample { int instanceLength; public Function<String,String> getStringOperation() { final String seperator = ":"; return target -> { int localLength = target.length(); instanceLength = target.length(); return target.toLowerCase() + seperator + instanceLength + seperator + localLength; }; } }
The lambda expression is created and used as shown here:
ClosureExample ce = new ClosureExample(); final Function<String,String> function = ce.getStringOperation(); System.out.println(function.apply("Closure"));
Its output follows:
closure:7:7
Variables used by the lambda expression are restricted in their use. Local variables or parameters cannot be redefined or modified. These variables need to be effectively final. That is, they must be declared as final or not be modified.
If the local variable and separator, had not been declared as final, the program would still be executed properly. However, if we tried to modify the variable later, then the following syntax error would be generated, indicating such variable was not permitted within a lambda expression:
local variables referenced from a lambda expression must be final or effectively final
If we add the following statements to the previous example and remove the final
keyword, we will get the same syntax error message:
function = String::toLowerCase; Consumer<String> consumer = s -> System.out.println(function.apply(s));
This is because the function
variable is used in the Consumer
lambda expression. It also needs to be effectively final, but we tried to assign a second value to it, the method reference for the toLowerCase
method.
Closure refers to functions that enclose variable external to the function. This permits the function to be passed around and used in different contexts.
Currying
Some functions can have multiple arguments. It is possible to evaluate these arguments one-by-one. This process is called currying and normally involves creating new functions, which have one fewer arguments than the previous one.
The advantage of this process is the ability to subdivide the execution sequence and work with intermediate results. This means that it can be used in a more flexible manner.
Consider a simple function such as:

The evaluation of f(2,3) will produce a 5. We could use the following, where the 2 is "hardcoded":

If we define:

Then the following are equivalent:

Substituting 3 for y we get:

This is the process of currying. An intermediate function, g(y), was introduced which we can pass around. Let's see, how something similar to this can be done in Java 8.
Start with a BiFunction
interface's apply
method that can be used for concatenation of strings. This method takes two parameters and returns a single value as implied by this lambda expression declaration:
BiFunction<String, String, String> biFunctionConcat = (a, b) -> a + b;
The use of the function is demonstrated with the following statement:
System.out.println(biFunctionConcat.apply("Cat", "Dog"));
The output will be the CatDog
string.
Next, let's define a reference variable called curryConcat
. This variable is a Function
interface variable. This interface is based on two data types. The first one is String
and represents the value passed to the Function
interface's accept
method. The second data type represents the accept
method's return type. This return type is defined as a Function
instance that is passed a string and returns a string. In other words, the curryConcat
function is passed a string and returns an instance of a function that is passed and returns a string.
Function<String, Function<String, String>> curryConcat;
We then assign an appropriate lambda expression to the variable:
curryConcat = (a) -> (b) -> biFunctionConcat.apply(a, b);
This may seem to be a bit confusing initially, so let's take it one piece at a time. First of all, the lambda expression needs to return a function. The lambda expression assigned to curryConcat
follows where the ellipses represent the body of the function. The parameter, a
, is passed to the body:
(a) ->...;
The actual body follows:
(b) -> biFunctionConcat.apply(a, b);
This is the lambda expression or function that is returned. This function takes two parameters, a
and b
. When this function is created, the a
parameter will be known and specified. This function can be evaluated later when the value for b
is specified. The function returned is an instance of a Function
interface, which is passed two parameters and returns a single value.
To illustrate this, define an intermediate variable to hold this returned function:
Function<String,String> intermediateFunction;
We can assign the result of executing the curryConcat
lambda expression using it's apply
method as shown here where a value of Cat
is specified for the a
parameter:
intermediateFunction = curryConcat.apply("Cat");
The next two statements will display the returned function:
System.out.println(intermediateFunction); System.out.println(curryConcat.apply("Cat"));
The output will look something similar to the following:
packt.Chapter2$$Lambda$3/798154996@5305068a packt.Chapter2$$Lambda$3/798154996@1f32e575
Note that these are the values representing this functions as returned by the implied toString
method. They are both different, indicating that two different functions were returned and can be passed around.
Now that we have confirmed a function has been returned, we can supply a value for the b
parameter as shown here:
System.out.println(intermediateFunction.apply("Dog"));
The output will be CatDog
. This illustrates how we can split a two parameter function into two distinct functions, which can be evaluated when desired. They can be used together as shown with these statements:
System.out.println(curryConcat.apply("Cat").apply("Dog")); System.out.println(curryConcat.apply( "Flying ").apply("Monkeys"));
The output of these statements is as follows:
CatDog Flying Monkeys
We can define a similar operation for doubles as shown here:
Function<Double, Function<Double, Double>> curryAdd = (a) -> (b) -> a * b; System.out.println(curryAdd.apply(3.0).apply(4.0));
This will display 12.0
as the returned value.
Currying is a valuable approach useful when the arguments of a function need to be evaluated at different times.
- Computer Vision for the Web
- Visual Basic 6.0程序設計計算機組裝與維修
- Visual C++實例精通
- Python爬蟲開發與項目實戰
- Podman實戰
- Mastering Python Networking
- HTML5+CSS3 Web前端開發技術(第2版)
- C/C++數據結構與算法速學速用大辭典
- 計算機應用基礎教程(Windows 7+Office 2010)
- Scratch·愛編程的藝術家
- SQL Server 2008 R2數據庫技術及應用(第3版)
- 零基礎學HTML+CSS
- Android Studio開發實戰:從零基礎到App上線 (移動開發叢書)
- Monitoring Docker
- Getting Started with Web Components