Find element
Now we are familiar with this elegantly designed method of transforming collections, but it is useless for finding elements. But the filter method was born for this.
We now want to remove the names starting with N from a list of names. Of course there may be none, and the result may be an empty set. Let’s implement it first using the old method.
Copy the code code as follows:
final List<String> startsWithN = new ArrayList<String>();
for(String name : friends) {
if(name.startsWith("N")) {
startsWithN.add(name);
}
}
Writing so much code for such a simple event is quite verbose. We first create a variable and then initialize it to an empty collection. Then iterate through the original collection and find those names starting with the specified letter. If found, it is inserted into the collection.
Let’s use the filter method to reconstruct the above code to see how powerful it is.
Copy the code code as follows:
final List<String> startsWithN =
friends.stream()
.filter(name -> name.startsWith("N"))
.collect(Collectors.toList());
The filter method receives a lambda expression that returns a Boolean value. If the expression evaluates to true, that element in the execution context is added to the result set; if not, it is skipped. What is finally returned is a Steam, which contains only those elements whose expression returns true. Finally we use a collect method to convert the collection into a list - we will discuss this method in more depth in Using the collect method and the Collecters class on page 52.
Let's print the elements in this result set:
Copy the code code as follows:
System.out.println(String.format("Found %d names", startsWithN.size()));
It is obvious from the output that this method has found all matching elements in the collection.
Copy the code code as follows:
Found 2 names
The filter method, like the map method, also returns an iterator, but that's about it. The collection returned by map is the same size as the input collection, but it is hard to say what filter returns. The size range of the set it returns, from 0 to the number of elements in the input set. Unlike map, filter returns a subset of the input set.
So far, we are very satisfied with the code simplicity brought by lambda expressions, but if we are not careful, the problem of code redundancy will slowly begin to grow. Let’s discuss this issue below.
Reuse of lambda expressions
Lambda expressions look very concise, but in fact it is easy to make code redundant if you are not careful. Redundancy will lead to low code quality and difficulty in maintenance; if we want to make a change, we have to change several related codes together.
Avoiding redundancy can also help us improve performance. The relevant code is concentrated in one place, so that we can analyze its performance and then optimize the code here, which can easily improve the performance of the code.
Now let's take a look at why using lambda expressions can easily lead to code redundancy, and consider how to avoid it.
Copy the code code as follows:
final List<String> friends =
Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
final List<String> editors =
Arrays.asList("Brian", "Jackie", "John", "Mike");
final List<String> comrades =
Arrays.asList("Kate", "Ken", "Nick", "Paula", "Zach");
We want to filter out names that start with a certain letter.
We want to filter names starting with a certain letter. Let’s simply implement it using the filter method first.
Copy the code code as follows:
final long countFriendsStartN =
friends.stream()
.filter(name -> name.startsWith("N")).count();
final long countEditorsStartN =
editors.stream()
.filter(name -> name.startsWith("N")).count();
final long countComradesStartN =
comrades.stream()
.filter(name -> name.startsWith("N")).count();
Lambda expressions make the code look concise, but it unknowingly brings redundancy to the code. In the above example, if we want to change the lambda expression, we have to change more than one place - which is not possible. Fortunately, we can assign lambda expressions to variables and reuse them just like objects.
The filter method, the receiver of the lambda expression, receives a reference to the java.util.function.Predicate functional interface. Here, the Java compiler comes in handy again. It generates an implementation of Predicate's test method using the specified lambda expression. Now we can more explicitly ask the Java compiler to generate this method instead of generating it where the parameters are defined. In the above example, we can explicitly store the lambda expression into a reference of type Predicate, and then pass this reference to the filter method; this can easily avoid code redundancy.
Let's refactor the previous code to make it comply with the DRY principle. (Don't Repeat Yoursef - DRY - principle, please refer to the book The Pragmatic Programmer: From Journeyman to Master [HT00]).
Copy the code code as follows:
final Predicate<String> startsWithN = name -> name.startsWith("N");
final long countFriendsStartN =
friends.stream()
.filter(startsWithN)
.count();
final long countEditorsStartN =
editors.stream()
.filter(startsWithN)
.count();
final long countComradesStartN =
comrades.stream()
.filter(startsWithN)
.count();
Now instead of writing the lambda expression again, we write it once and store it in a reference of type Predicate called startsWithN. In the following three filter calls, the Java compiler saw the lambda expression disguised as Predicate, smiled and accepted it silently.
This newly introduced variable eliminates code redundancy for us. But unfortunately, as we will see later, the enemy will soon come back to take revenge. Let's see what more powerful weapons can destroy them for us.