Translation annotation: map (mapping) and reduce (reduce, simplify) are two very basic concepts in mathematics. They have appeared in various functional programming languages for a long time. It was not until 2003 that Google carried them forward and applied them to After parallel computing was implemented in distributed systems, the name of this combination began to shine in the computer world (those functional fans may not think so). In this article, we will see the debut of map and reduce combinations after Java 8 supports functional programming (this is just a preliminary introduction, there will be special topics on them later).
Reduce a set
So far we have introduced several new techniques for operating collections: finding matching elements, finding individual elements, and collection transformations. These operations have one thing in common, they all operate on a single element in the collection. There is no need to compare elements or perform operations on two elements. In this section we look at how to compare elements and dynamically maintain an operation result during collection traversal.
Let's start with simple examples and then work our way up. In the first example, we first iterate through the friends collection and calculate the total number of characters in all names.
Copy the code code as follows:
System.out.println("Total number of characters in all names: " + friends.stream()
.mapToInt(name -> name.length())
.sum());
To calculate the total number of characters we need to know the length of each name. This can be easily accomplished through the mapToInt() method. After we have converted the names into corresponding lengths, we only need to add them together in the end. We have a built-in sum() method to accomplish this. Here is the final output:
Copy the code code as follows:
Total number of characters in all names: 26
We used a variant of the map operation, the mapToInt() method (such as mapToInt, mapToDouble, etc., which will generate specific types of streams, such as IntStream, DoubleStream), and then calculated the total number of characters based on the returned length.
In addition to using the sum method, there are many similar methods that can be used, such as max() to find the maximum length, min() to find the minimum length, sorted() to sort the lengths, average() to find the average length, etc. wait.
Another attractive aspect of the above example is the increasingly popular MapReduce mode. The map() method performs mapping, and the sum() method is a commonly used reduce operation. In fact, the implementation of the sum() method in the JDK uses the reduce() method. Let's take a look at some of the more commonly used forms of reduce operations.
For example, we iterate through all the names and print the one with the longest name. If there are several longest names, we print the one found first. One way is that we calculate the maximum length and then select the first element that matches this length. However, doing this requires traversing the list twice - which is too inefficient. This is where the reduce operation comes into play.
We can use the reduce operation to compare the lengths of two elements, then return the longest one, and further compare it with the remaining elements. Like other higher-order functions we saw before, the reduce() method also traverses the entire collection. Among other things, it records the calculation result returned by the lambda expression. If there is an example that can help us understand this better, let's look at a piece of code first.
Copy the code code as follows:
final Optional<String> aLongName = friends.stream()
.reduce((name1, name2) ->
name1.length() >= name2.length() ? name1 : name2);
aLongName.ifPresent(name ->
System.out.println(String.format("A longest name: %s", name)));
The lambda expression passed to the reduce() method receives two parameters, name1 and name2, and it compares their lengths and returns the longest one. The reduce() method has no idea what we are going to do. This logic is stripped out into the lambda expression we pass in - this is a lightweight implementation of the Strategy pattern.
This lambda expression can be adapted to the apply method of the functional interface of a BinaryOperator in the JDK. This is exactly the type of argument the reduce method accepts. Let's run this reduce method and see if it can correctly select the first of the two longest names.
Copy the code code as follows:
A longest name: Brian
When the reduce() method traverses the collection, it first calls the lambda expression on the first two elements of the collection, and the result returned by the call continues to be used for the next call. In the second call, the value of name1 is bound to the result of the previous call, and the value of name2 is the third element of the collection. The remaining elements are also called in this order. The result of the last lambda expression call is the result returned by the entire reduce() method.
The reduce() method returns an Optional value because the collection passed to it may be empty. In that case, there would be no longest name. If the list has only one element, the reduce method directly returns that element and does not call the lambda expression.
From this example we can infer that the result of reduce can only be at most one element in the set. If we wish to return a default or base value, we can use a variant of the reduce() method that accepts an additional parameter. For example, if the shortest name is Steve, we can pass it to the reduce() method like this:
Copy the code code as follows:
final String steveOrLonger = friends.stream()
.reduce("Steve", (name1, name2) ->
name1.length() >= name2.length() ? name1 : name2);
If there is a name longer than it, then this name will be selected; otherwise, the base value Steve will be returned. This version of the reduce() method does not return an Optional object, because if the collection is empty, a default value will be returned; regardless of the case where there is no return value.
Before we end this chapter, let's take a look at a very basic but not so easy operation in set operations: merging elements.
Merge elements
We have learned how to find elements, traverse, and convert collections. However, there is another common operation - splicing collection elements - without this newly added join() function, the concise and elegant code mentioned before would be in vain. This simple method is so practical that it has become one of the most commonly used functions in the JDK. Let's see how to use it to print elements in a list, separated by commas.
We still use this friends list. If you use the old method in the JDK library, what should you do if you want to print out all the names separated by commas?
We have to iterate through the list and print the elements one by one. The for loop in Java 5 is improved over the previous one, so let’s use it.
Copy the code code as follows:
for(String name : friends) {
System.out.print(name + ", ");
}
System.out.println();
The code is very simple, let's see what its output is.
Copy the code code as follows:
Brian, Nate, Neal, Raju, Sara, Scott,
Damn, there's that annoying comma at the end (can we blame that Scott at the end?). How can I tell Java not to put a comma here? Unfortunately, the loop executes step by step, and it's not easy to do something special at the end. In order to solve this problem, we can use the original loop method.
Copy the code code as follows:
for(int i = 0; i < friends.size() - 1; i++) {
System.out.print(friends.get(i) + ", ");
}
if(friends.size() > 0)
System.out.println(friends.get(friends.size() - 1));
Let’s see if the output of this version is OK.
Copy the code code as follows:
Brian, Nate, Neal, Raju, Sara, Scott
The result is still good, but this code is not flattering. Save us, Java.
We don’t have to endure this pain anymore. The StringJoiner class in Java 8 helps us solve these problems. Not only that, the String class also adds a join method so that we can replace the above stuff with one line of code.
Copy the code code as follows:
System.out.println(String.join(", ", friends));
Come take a look, the results are as satisfying as the code.
Copy the code code as follows:
Brian, Nate, Neal, Raju, Sara, Scott
The result is still good, but this code is not flattering. Save us, Java.
We don’t have to endure this pain anymore. The StringJoiner class in Java 8 helps us solve these problems. Not only that, the String class also adds a join method so that we can replace the above stuff with one line of code.
Copy the code code as follows:
System.out.println(String.join(", ", friends));
Come take a look, the results are as satisfying as the code.
Copy the code code as follows:
Brian, Nate, Neal, Raju, Sara, Scott
In the underlying implementation, the String.join() method calls the StringJoiner class to splice the value passed in as the second parameter (which is a variable-length parameter) into a long string, using the first parameter as the delimiter. Of course, this method is more than just splicing commas. For example, we can pass in a bunch of paths and easily spell out a classpath, thanks to these newly added methods and classes.
We already know how to connect list elements. Before connecting lists, we can also transform the elements. Of course, we also know how to use the map method to transform lists. Next, we can also use the filter() method to filter out the elements we want. The last step of connecting the list elements, using commas or some other delimiter, is just a simple reduce operation.
We can use the reduce() method to concatenate the elements into a string, but this requires some work on our part. JDK has a very convenient collect() method, which is also a variant of reduce(). We can use it to combine elements into a desired value.
The collect() method performs the reduction operation, but it delegates the specific operation to a collector for execution. We can merge the converted elements into an ArrayList. Continuing with the previous example, we can concatenate the converted elements into a comma-separated string.
Copy the code code as follows:
System.out.println(
friends.stream()
.map(String::toUpperCase)
.collect(joining(", ")));
We called the collect() method on the converted list, passing it a collector returned by the joining() method. Joining is a static method in the Collectors tool class. The collector is like a receiver. It receives the objects passed in by collect and stores them in the format you want: ArrayList, String, etc. We will explore this method further in the collect method and Collectors class on page 52.
This is the output name, now they are uppercase and separated by commas.
Copy the code code as follows:
BRIAN, NATE, NEAL, RAJU, SARA, SCOTT
Summarize
Collections are very common in programming. With lambda expressions, Java's collection operations have become simpler and easier. All the clunky old code for collection operations can be replaced with this elegant and concise new approach. The internal iterator makes collection traversal and transformation more convenient, away from the trouble of variability, and finding collection elements becomes extremely easy. You can write a lot less code using these new methods. This makes the code easier to maintain, more focused on business logic, and less basic operations in programming.
In the next chapter we'll see how lambda expressions simplify another basic operation in program development: string manipulation and object comparison.