Chapter 2: Use of Collections
We often use various collections, numbers, strings and objects. They are everywhere, and even if the code that operates the collection can be slightly optimized, it will make the code much clearer. In this chapter, we explore how to use lambda expressions to manipulate collections. We use it to traverse collections, convert collections into new collections, remove elements from collections, and merge collections.
Traverse the list
Traversing a list is the most basic set operation, and its operations have undergone some changes over the years. We use a small example of traversing names, introducing it from the oldest version to the most elegant version today.
We can easily create an immutable list of names with the following code:
Copy the code code as follows:
final List<String> friends =
Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
System.out.println(friends.get(i));
}
The following is the most common method of traversing a list and printing it, although it is also the most general:
Copy the code code as follows:
for(int i = 0; i < friends.size(); i++) {
System.out.println(friends.get(i));
}
I call this way of writing masochistic--it's verbose and error-prone. We have to stop and think about it, "Is it i< or i<=?" This only makes sense when we need to operate on a specific element, but even then, we can still use functional expressions that adhere to the principle of immutability. style, which we will discuss shortly.
Java also provides a relatively advanced for structure.
Copy the code code as follows:
collections/fpij/Iteration.java
for(String name : friends) {
System.out.println(name);
}
Under the hood, iteration in this manner is implemented using the Iterator interface, calling its hasNext and next methods. Both methods are external iterators, and they combine how to do it with what you want to do. We explicitly control the iteration, telling it where to start and where to end; the second version does this under the hood through the Iterator method. Under explicit operation, you can also use break and continue statements to control iteration. The second version has a few things missing from the first. This approach is better than the first one if we don't intend to modify an element of the collection. However, both of these methods are imperative and should be abandoned in current Java. There are several reasons for changing to functional style:
1. The for loop itself is serial and difficult to parallelize.
2. Such a loop is non-polymorphic; what you get is what you ask for. We pass the collection directly to the for loop, rather than calling a method (which supports polymorphism) on the collection to perform a specific operation.
3. From a design perspective, code written in this way violates the "Tell, Don't Ask" principle. We request that an iteration be performed rather than leaving the iteration to the underlying library.
It's time to switch from the old imperative programming to the more elegant functional programming of internal iterators. After using internal iterators, we leave many specific operations to the underlying method library for execution, so you can focus more on specific business requirements. The underlying function will be responsible for iteration. We first use an internal iterator to enumerate the list of names.
The Iterable interface has been enhanced in JDK8. It has a special name called forEach, which receives a parameter of type Comsumer. As the name says, a Consumer instance consumes the object passed to it through its accept method. We use the familiar anonymous inner class syntax to use the forEach method:
Copy the code code as follows:
friends.forEach(new Consumer<String>() { public void accept(final String name) {
System.out.println(name); }
});
We called the forEach method on the friends collection, passing it an anonymous implementation of Consumer. This forEach method calls the accept method of the passed in Consumer for each element in the collection, allowing it to process this element. In this example we just print its value, which is the name. Let's take a look at the output of this version, which is the same as the previous two:
Copy the code code as follows:
Brian
Nate
Neal
Raju
Sara
Scott
We only changed one thing: we ditched the obsolete for loop and used a new internal iterator. The advantage is that we don't have to specify how to iterate the collection and can focus more on how to process each element. The disadvantage is that the code looks more verbose - which almost kills the joy of the new coding style. Fortunately, this is easy to change, and this is where the power of lambda expressions and new compilers comes into play. Let's make one more modification and replace the anonymous inner class with a lambda expression.
Copy the code code as follows:
friends.forEach((final String name) -> System.out.println(name));
It looks much better this way. There's less code, but let's first look at what that means. The forEach method is a higher-order function that receives a lambda expression or code block to operate on the elements in the list. At each call, the elements in the collection will be bound to the name variable. The underlying library hosts the activity of lambda expression invocation. It can decide to delay the execution of expressions and, if appropriate, perform parallel computations. The output of this version is also the same as the previous one.
Copy the code code as follows:
Brian
Nate
Neal
Raju
Sara
Scott
The internal iterator version is more concise. Moreover, by using it, we can focus more on the processing of each element instead of traversing it - this is declarative.
However, this version has flaws. Once the forEach method starts executing, unlike the other two versions, we cannot break out of this iteration. (Of course there are other ways to do this). Therefore, this way of writing is more commonly used when each element in the collection needs to be processed. Later we will introduce some other functions that allow us to control the loop process.
The standard syntax for lambda expressions is to put the parameters inside (), provide type information and use commas to separate the parameters. In order to liberate us, the Java compiler can also automatically perform type deduction. Of course it is more convenient not to write types. There is less work and the world is quieter. The following is the previous version after removing the parameter type:
Copy the code code as follows:
friends.forEach((name) -> System.out.println(name));
In this example, the Java compiler knows that the type of name is String through context analysis. It looks at the signature of the called method forEach and then analyzes the functional interface in the parameters. Then it will analyze the abstract method in this interface and check the number and type of parameters. Even if this lambda expression receives multiple parameters, we can still perform type deduction, but in this case all parameters cannot have parameter types; in lambda expressions, the parameter types must be written at all, or if they are written, they must be written in full.
The Java compiler treats lambda expressions with a single parameter specially: if you want to perform type inference, the parentheses around the parameter can be omitted.
Copy the code code as follows:
friends.forEach(name -> System.out.println(name));
There is a small caveat here: the parameters used for type inference are not of final type. In the previous example of explicitly declaring a type, we also marked the parameter as final. This prevents you from changing the value of the parameter in the lambda expression. Generally speaking, it is a bad habit to modify the value of a parameter, which can easily cause BUG, so it is a good habit to mark it as final. Unfortunately, if we want to use type inference, we have to follow the rules ourselves and not modify the parameters, because the compiler no longer protects us.
It took a lot of effort to get to this point, but now the amount of code is indeed a little smaller. But this is not the simplest yet. Let’s try this last minimalist version.
Copy the code code as follows:
friends.forEach(System.out::println);
In the above code we use a method reference. We can directly replace the entire code with the method name. We’ll explore this in depth in the next section, but for now let’s recall a famous quote from Antoine de Saint-Exupéry: Perfection is not what can be added, but what can no longer be taken away.
Lambda expressions allow us to traverse collections concisely and clearly. In the next section, we will talk about how it enables us to write such concise code when performing deletion operations and collection conversions.