Using lexical scope and closures
Many developers have this misunderstanding, believing that using lambda expressions will lead to code redundancy and reduce code quality. On the contrary, no matter how complex the code becomes, we will not make any compromises on code quality for the sake of simplicity, as we will see below.
We were able to reuse the lambda expression in the previous example; however, if we match another letter, the problem of code redundancy quickly returns. Let's analyze this problem further first, and then use lexical scope and closures to solve it.
Redundancy caused by lambda expressions
Let's filter out those letters starting with N or B from friends. Continuing with the above example, the code we write might look like this:
Copy the code code as follows:
final Predicate<String> startsWithN = name -> name.startsWith("N");
final Predicate<String> startsWithB = name -> name.startsWith("B");
final long countFriendsStartN =
friends.stream()
.filter(startsWithN).count();
final long countFriendsStartB =
friends.stream()
.filter(startsWithB).count();
The first predicate determines whether the name starts with N, and the second one determines whether the name begins with B. We pass these two instances to two filter method calls respectively. This seems reasonable, but the two predicates are redundant, they are just different letters in the check. Let's see how we can avoid this redundancy.
Use lexical scope to avoid redundancy
In the first solution, we can extract the letters as parameters of the function and pass this function to the filter method. This is a good method, but filter is not accepted by all functions. It only accepts functions with only one parameter. That parameter corresponds to the element in the collection and returns a boolean value. It hopes that what is passed in is a Predicate.
We hope there is a place where this letter can be cached until the parameter is passed (in this case, the name parameter). Let's create a new function like this.
Copy the code code as follows:
public static Predicate<String> checkIfStartsWith(final String letter) {
return name -> name.startsWith(letter);
}
We defined a static function checkIfStartsWith, which receives a String parameter and returns a Predicate object, which can be passed to the filter method for later use. Unlike the higher-order functions we saw earlier, which take functions as parameters, this method returns a function. But it is also a higher-order function, which we have already mentioned in Evolution, not change, on page 12.
The Predicate object returned by the checkIfStartsWith method is somewhat different from other lambda expressions. In the return name -> name.startsWith(letter) statement, we know exactly what name is, it is the parameter passed into the lambda expression. But what exactly is the variable letter? It is outside the domain of the anonymous function. Java finds the domain where the lambda expression is defined and discovers the variable letter. This is called lexical scope. Lexical scope is a very useful thing, it allows us to cache a variable in one scope for later use in another context. Because this lambda expression uses variables in its scope, this situation is also called a closure. Regarding the access restrictions of lexical scope, can you read the restrictions on lexical scope on page 31?
Are there any restrictions on lexical scope?
In a lambda expression, we can only access final types in its scope or actually local variables of final type.
The lambda expression may be called immediately, delayed, or from a different thread. In order to avoid race conflicts, the local variables in the domain we access are not allowed to be modified once initialized. Any modification operation will cause compilation exception.
Marking it as final solves this problem, but Java does not force us to mark it this way. In fact, Java looks at two things. One is that the variable accessed must be initialized in the method in which it is defined, and before the lambda expression is defined. Second, the values of these variables cannot be modified - that is, they are in fact of type final, although not marked as such.
Stateless lambda expressions are runtime constants, while those using local variables have additional computational overhead.
When calling the filter method, we can use the lambda expression returned by the checkIfStartsWith method, like this:
Copy the code code as follows:
final long countFriendsStartN =
friends.stream() .filter(checkIfStartsWith("N")).count();
final long countFriendsStartB = friends.stream()
.filter(checkIfStartsWith("B")).count();
Before calling the filter method, we first called the checkIfStartsWith() method and passed in the desired letters. This call quickly returns a lambda expression, which we then pass to the filter method.
By creating a higher-order function (checkIfStartsWith in this case) and using lexical scope, we successfully removed redundancy from the code. We no longer need to repeatedly determine whether the name starts with a certain letter.
Refactor, reduce scope
In the previous example we used a static method, but we don't want to use the static method to cache variables, which will mess up our code. It's best to narrow the scope of this function to the place where it is used. We can use a Function interface to achieve this.
Copy the code code as follows:
final Function<String, Predicate<String>> startsWithLetter = (String letter) -> {
Predicate<String> checkStarts = (String name) -> name.startsWith(letter);
return checkStarts; };
This lambda expression replaces the original static method. It can be placed in a function and defined before it is needed. The startWithLetter variable refers to a Function whose input parameter is String and whose output parameter is Predicate.
Compared with using the static method, this version is much simpler, but we can continue to refactor it to make it more concise. From a practical point of view, this function is the same as the previous static method; they both receive a String and return a Predicate. Instead of explicitly declaring a Predicate, we replace it entirely with a lambda expression.
Copy the code code as follows:
final Function<String, Predicate<String>> startsWithLetter = (String letter) -> (String name) -> name.startsWith(letter);
We've gotten rid of the mess, but we can also remove the type declaration to make it more concise, and the Java compiler will do type deduction based on the context. Let's take a look at the improved version.
Copy the code code as follows:
final Function<String, Predicate<String>> startsWithLetter =
letter -> name -> name.startsWith(letter);
It takes some effort to adapt to this concise syntax. If it blinds you, look elsewhere first. We have completed the refactoring of the code and can now use it to replace the original checkIfStartsWith() method, like this:
Copy the code code as follows:
final long countFriendsStartN = friends.stream()
.filter(startsWithLetter.apply("N")).count();
final long countFriendsStartB = friends.stream()
.filter(startsWithLetter.apply("B")).count();
In this section we use higher-order functions. We saw how to create functions within functions if we pass a function to another function, and how to return a function from a function. These examples all demonstrate the simplicity and reusability brought by lambda expressions.
In this section we have fully utilized the functions of Function and Predicate, but let's take a look at the difference between them. Predicate accepts a parameter of type T and returns a boolean value to represent the true or false of its corresponding judgment condition. When we need to make conditional judgments, we can use Predicateg to complete it. Methods like filter that filter elements receive Predicate as a parameter. Funciton represents a function whose input parameters are variables of type T and returns a result of type R. It is more general than Predicate which can only return boolean. As long as the input is converted into an output, we can use Function, so it is reasonable for map to use Function as a parameter.
As you can see, selecting elements from a collection is very simple. Below we will introduce how to select only one element from the collection.