Functional Programming in Java venkat(5): Using Collections part3
Introduction
这里是记录学习这本书 Functional Programming in Java: Harnessing the Power Of Java 8 Lambda Expressions 的读书笔记,如有侵权,请联系删除。
About the author
Venkat Subramaniam
Dr. Venkat Subramaniam, founder of Agile Developer, Inc., has trained and mentored thousands of software developers in the US, Canada, Europe, and Asia. Venkat helps his clients effectively apply and succeed with agile practices on their software projects. He is a frequent invited speaker at international software conferences and user groups. He’s author of .NET Gotchas (O’Reilly), coauthor of the 2007 Jolt Productivity award-winning book Practices of an Agile Developer (Pragmatic Bookshelf),
Using Collections
Picking an Element
按理来说,从collection中选一个元素要比选出很多元素要简单,但是有一些需要注意的地方。
It’s reasonable to expect that picking one element from a collection would be simpler than picking multiple elements. But there are a few complications. Let’s look at the complexity introduced by the habitual approach and then bring in lambda expressions to solve it.
需求:从collection中选出第一个以某个字母开头的名字
Let’s create a method that will look for an element that starts with a given letter, and print it.
最初的方法:
package fpij;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import static fpij.Folks.friends;
public class PickAnElement {
public static void pickName(
final List<String> names, final String startingLetter) {
String foundName = null;
for(String name : names) {
if(name.startsWith(startingLetter)) {
foundName = name;
break;
}
}
System.out.print(String.format("A name starting with %s: ", startingLetter));
if(foundName != null) {
System.out.println(foundName);
} else {
System.out.println("No name found");
}
}
public static void main(final String[] args) {
pickName(friends, "N");
pickName(friends, "Z");
}
}
这种方法的气味很容易与经过的垃圾车竞争(言外之意,这个方法很差)
首先用了null,这要程序员必须进行多余的检查,否则容易出现NullPointerException或者意想不到的问题。
其次, 用了外部迭代:命令式,带来mutability。
另外,一个简单的任务,写了这么多代码。
This method’s odor can easily compete with passing garbage trucks. We first created a foundName variable and initialized it to null—that’s the source of our first bad smell. This will force a null check, and if we forget to deal with it the result could be a NullPointerException or an unpleasant response. We then used an external iterator to loop through the elements, but had to break out of the loop if we found an element—here are other sources of rancid smells: primitive obsession, imperative style, and mutability. Once out of the loop, we had to check the response and print the appropriate result. That’s quite a bit of code for a simple task.
重写pickName方法:使用lambda表达式
Let’s rethink the problem. We simply want to pick the first matching element and safely deal with the absence of such an element. Let’s rewrite the pickName() method, this time using lambda expressions.
public static void pickName(
final List<String> names, final String startingLetter) {
final Optional<String> foundName =
names.stream()
.filter(name ->name.startsWith(startingLetter))
.findFirst();
System.out.println(String.format("A name starting with %s: %s",
startingLetter, foundName.orElse("No name found")));
}
自带的库函数登场,帮我们实现一种简洁和安全的方式。
使用流方法,先用filter过滤出来以某个字母开始的名字,再用findFirst方法来输出第一个找到的。
返回的是Optional类型
Some powerful features in the JDK library came together to help achieve this conciseness. First we used the filter() method to grab all the elements matching the desired pattern. Then the findFirst() method of the Stream class helped pick the first value from that collection. This method returns a special Optional object, which is the state-appointed null deodorizer in Java.
Optional类的适用场合:当可能有返回值的时候,如果没有找到,它会保护我们不被空指针异常叨扰,显式地表示出来,方便我们处理。
The Optional class is useful whenever there may be a result. It protects us from getting a NullPointerException by accident and makes it quite explicit to the reader that “no result found” is a possible outcome. We can inquire if an object is present by using the isPresent() method, and we can obtain the current value using its get() method. Alternatively, we could suggest a substitute value for the missing instance, using the method orElse(), like in the previous code.
现在来跑一下这个代码
Let’s exercise the pickName() function with the sample friends collection we’ve used in the examples so far.
pickName(friends, "N");
pickName(friends, "Z");
简单解释
The code picks out the first matching element, if found, and prints an appropriate message otherwise.
这里的findFirst方法和Optional类组合使用,让代码安全不少,更简洁,reduce smell。 但是Optional还有其他用法,比如只有元素存在时,才执行一段lambda表达式,看下面的代码
The combination of the findFirst() method and the Optional class reduced our code and its smell quite a bit. We’re not limited to the preceding options when working with Optional, though. For example, rather than providing an alternate value for the absent instance, we can ask Optional to run a block of code or a lambda expression only if a value is present, like so:
final Optional<String> foundName =
friends.stream()
.filter(name ->name.startsWith("N"))
.findFirst();
System.out.println("//" + "START:CLOSURE_OUTPUT");
foundName.ifPresent(name -> System.out.println("Hello " + name));
System.out.println("//" + "END:CLOSURE_OUTPUT");
使用这种functional的代码,还有很多东西好处,比如leverage the laziness of Streams,后面会讲。
When compared to using the imperative version to pick the first matching name, the nice, flowing functional style looks better. But are we doing more work in the fluent version than we did in the imperative version? The answer is no—these methods have the smarts to perform only as much work as is necessary (we’ll talk about this more in Leveraging the Laziness of Streams, on page 113).
上面是使用lambda表达式选出第一个符合条件的值,下一节使用lambda表达式对collection进行计算,只返回一个值。
The search for the first matching element demonstrated a few more neat capabilities in the JDK. Next we’ll look at how lambda expressions help compute a single result from a collection.
Reducing a Collection to a Single Value
如何比较元素,并且对collection进行计算输出。
We’ve gone over quite a few techniques to manipulate collections so far: picking matching elements, selecting a particular element, and transforming a collection. All these operations have one thing in common: they all worked independently on individual elements in the collection. None required comparing elements against each other or carrying over computations from one element to the next. In this section we look at how to compare elements and carry over a computational state across a collection.
从简单的例子开始:计算全部名称字母的数量
Let’s start with some basic operations and build up to something a bit more sophisticated. As the first example, let’s read over the in friends collection of names and determine the total number of characters.
System.out.println("Total number of characters in all names: " +
friends.stream()
.mapToInt(name -> name.length())
.sum());
mapToInt()方法把名字转化为长度,然后sum求和。
To find the total of the characters we need the length of each name. We can easily compute that using the mapToInt() method. Once we transform from the names to their lengths, the final step is to total them. This step we perform using the built-in sum() method. Here’s the output for this operation:
Total number of characters in all names: 26
mapToInt是map函数的变种,可以创建特定类型的流。
We leveraged the mapToInt() method, a variation of the map operation (variations like mapToInt(), mapToDouble(), and so on create type-specialized steams such as IntStream and DoubleStream) and then reduced the resulting length to the sum value.
除了用sum函数,还可以用max,min或者average,以及sorted方法来处理各个元素。
Instead of using the sum() method, we could use a variety of methods like max() to find the longest length, min() to find the shortest length, sorted() to sort the lengths, average() to find the average of the length, and so on.
map-reduce这个模式很火。
The hidden charm in the preceding example is the increasingly popular map-reduce pattern,2 with the map() method being the spread operation and the sum() method being the special case of the more general reduce operation. In fact, the implementation of the sum() method in the JDK uses a reduce() method. Let’s look at the more general form of reduce.
下面介绍reduce方法
As an example, let’s read over the given collection of names and display the longest one. If there is more than one name with the same highest length, we’ll display the first one we find. One way we could do that is to figure out the longest length, and then pick the first element of that length. But that’d require going over the list twice—not efficient. This is where a reduce() method comes into play.
可以使用reduce方法来比较两个元素,然后把比较结果和collection中的其他元素比较。
We can use the reduce() method to compare two elements against each other and pass along the result for further comparison with the remaining elements in the collection. Much like the other higher-order functions on collections we’ve seen so far, the reduce() method iterates over the collection. In addition, it carries forward the result of the computation that the lambda expression returned. An example will help clarify this, so let’s get down to the code.
下面是一个reduce的例子
public static final List<String> friends =
Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
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)));
这里reduce接收一个lambda表达式:两个参数,返回长度更长的那一个。
这是策略模式(一种设计模式)的轻量型应用。
The lambda expression we’re passing to the reduce() method takes two parameters, name1 and name2, and returns one of them based on the length. The reduce() method has no clue about our specific intent. That concern is separated from this method into the lambda expression that we pass to it— this is a lightweight application of the strategy pattern.
此 lambda 表达式适配于名为 BinaryOperator 的 JDK 功能接口的 apply() 方法的接口
This lambda expression conforms to the interface of an apply() method of a JDK functional interface named BinaryOperator. This is the type of the parameter the reduce() method receives. Let’s run the reduce() method and see if it picks the first of the two longest names from our friends list.
A longest name: Brian
reduce的工作过程,首先对collection的前两个元素使用lambda表达式,结果再和下一个紧邻的元素执行lambda表达式,以此类推。最后比较的结果就是真个reduce的结果
As the reduce() method iterated through the collection, it called the lambda expression first, with the first two elements in the list. The result from the lambda expression is used for the subsequent call. In the second call name1 is bound to the result from the previous call to the lambda expression, and name2 is bound to the third element in the collection. The calls to the lambda expression continue for the rest of the elements in the collection. The result from the final call is returned as the result of the reduce() method call.
reduce的结果是Optional类型的,因为reduce可能调用空的list,所以用Optional,它可以显式地处理空这种问题。
另外,如果list只有一个元素,reduce就会返回这个元素,不会调用lambda表达式。
The result of the reduce() method is an Optional because the list on which reduce() is called may be empty. In that case, there would be no longest name. If the list had only one element, then reduce() would return that element and the lambda expression we pass would not be invoked.
reduce的结果是最多只有一个元素,另外我们可以给reduce方法设置一个基础值。
From the example we can infer that the reduce() method’s result is at most one element from the collection. If we want to set a default or a base value, we can pass that value as an extra parameter to an overloaded variation of the reduce() method. For example, if the shortest name we want to pick is Steve, we can pass that to the reduce() method, like so:
final String steveOrLonger =
friends.stream()
.reduce("Steve", (name1, name2) ->
name1.length() >= name2.length() ? name1 : name2);
System.out.println(steveOrLonger);
这段代码的含义是,后面的lambda表达式还是返回长度更长的名字,只不过现在给定一个base value: Steve,只要比他名字长的才会返回,而且是返回第一个比他长的名字。
If any name was longer than the given base, it would get picked up; otherwise the function would return the base value, Steve in this example. This version of reduce() does not return an Optional since if the collection is empty, the default will be returned; there’s no concern of an absent or nonexistent value.
笔者自己测试了代码
public static final List<String> friends =
Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
final String steveOrLonger =
friends.stream()
.reduce("Steve", (name1, name2) ->
{
System.out.println("name1: " + name1 + " name2: " + name2);
return name1.length() >= name2.length() ? name1 : name2;
});
System.out.println(steveOrLonger);
测试结果
name1: Steve name2: Brian
name1: Steve name2: Nate
name1: Steve name2: Neal
name1: Steve name2: Raju
name1: Steve name2: Sara
name1: Steve name2: Scott
Steve
测试的目的是什么呢?笔者想看看lambda表达式的两个参数,到底是第一个参数表示默认值还是第二个参数表示默认值?
这里发现第一个参数表示初始值,这里是Steve。
下一节我们学习joining elements
Before we wrap up this chapter, let’s visit a fundamental yet seemingly difficult operation on collections: joining elements.
Joining Elements
如何concatenating一个集合,比如把每个元素之间用逗号隔开?
join方法可能是最有用的方法之一。
We’ve explored how to select elements, iterate, and transform collections. Yet in a trivial operation—concatenating a collection—we could lose all the gains we made with concise and elegant code if not for a newly added join() function. This simple method is so useful that it’s poised to become one of the most used functions in the JDK. Let’s see how to use it to print the values in a list, comma separated.
现在来看具体例子
Let’s work with our friends list. What does it take to print the list of names, separated by commas, using only the old JDK libraries?
old style java,但是是使用forEach
We have to iterate through the list and print each element. Since the Java 5 for construct is better than the archaic for loop, let’s start with that.
for(String name : friends) {
System.out.print(name + ", ");
}
但是最后一个没有特殊处理,这样的话后面会有一个多余的逗号
That was simple code, but let’s look at what it yielded.
Brian, Nate, Neal, Raju, Sara, Scott,
使用旧方法来处理,直接用for 循环,很ugly
Darn it; there’s a stinking comma at the end (shall we blame it on Scott?). How do we tell Java not to place a comma there? Unfortunately, the loop will run its course and there’s no easy way to tell the last element apart from the rest. To fix this, we can fall back on the habitual loop.
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 was decent.
Brian, Nate, Neal, Raju, Sara, Scott
The result looks good, but the code to produce the output does not. Beam us up, modern Java.
java8处理这个问题处理得相当好,有一个类StringJoiner 来处理这种不elegant的情况,并且我们可以使用join方法。
We no longer have to endure that pain. A StringJoiner class cleans up all that mess in Java 8 and the String class has an added convenience method join() to turn that smelly code into a simple one-liner.
perfect!
System.out.println(String.join(", ", friends));
Let’s quickly verify the output is as charming as the code that produced it.
Brian, Nate, Neal, Raju, Sara, Scott
join方法底层做了什么呢?它调用StringJoiner来处理第二个参数(比如集合)中的每个值,中间用第一个参数(比如逗号)来分隔开,返回一个更大的集合。
Under the hood the String’s join() method calls upon the StringJoiner to concatenate the values in the second argument, a varargs, into a larger string separated by the first argument. We’re not limited to concatenating only with a comma using this feature. We could, for example, take a bunch of paths and concate-nate them to form a classpath quite easily, thanks to the new methods and classes
我们之前学过的filter,map,reduce和join可以连用。
We saw how to join a list of elements; we can also transform the elements before joining them. We already know how to transform elements using the map() method. We can also be selective about which element we want to keep by using methods like filter(). The final step of joining the elements, separated by commas or something else, is simply to use a reduce operation.
还有一个collect方法,帮助我们收集元素
We could use the reduce() method to concatenate elements into a string, but that would require some effort on our part. The JDK has a convenience method named collect(), which is another form of reduce that can help us collect values into a target destination.
比如把元素收集成一个ArrayList,收集成一个String等。
The collect() method does the reduction but delegates the actual implementation or target to a collector. We could drop the transformed elements into an ArrayList, for instance. Alternatively, to continue with the current example, we could collect the transformed elements into a string concatenated with commas.
System.out.println("//" + "START:MAP_JOIN_OUTPUT");
System.out.println(
friends.stream()
.map(String::toUpperCase)
.collect(joining(", ")));
System.out.println("//" + "END:MAP_JOIN_OUTPUT");
下面这一段使用google翻译:我们在转换后的列表上调用 collect() 并为其提供由 join() 方法返回的收集器,该方法是 Collectors 实用程序类上的静态方法。 收集器充当接收器对象,以接收由 collect() 方法传递的元素并将其存储为所需的格式:ArrayList、String 等。
We invoked the collect() on the transformed list and provided it a collector returned by the joining() method, which is a static method on a Collectors utility class. A collector acts as a sink object to receive elements passed by the collect() method and stores it in a desired format: ArrayList, String, and so on.
Here are the names, now in uppercase and comma separated.
BRIAN, NATE, NEAL, RAJU, SARA, SCOTT
StringJoiner类可以有很多用法。
The StringJoiner gives a lot more control over the format of concatenation; we can specify a prefix, a suffix, and infix character sequences, if we desire.
下面我们复习一下这一章所学的知识。
We saw how lambda expressions and the newly added classes and methods make programming in Java so much easier, and more fun too. Let’s go over what we covered in this chapter.
Recap
lambda表达式的出现,让Java处理collection更简单、优雅、简洁。
内部迭代器带来的:很容易遍历collection,不用带来mutability便可以transform collection,另外轻松处理select elements的问题。
写更少的代码,带来更容易维护的效果。
Collections are commonplace in programming and, thanks to lambda expressions, using them is now much easier and simpler in Java. We can trade the longwinded old methods for elegant, concise code to perform the common operations on collections. Internal iterators make it convenient to traverse collections, transform collections without enduring mutability, and select elements from collections without much effort. Using these functions means less code to write. That can lead to more maintainable code, more code that does useful domain- or application-related logic, and less code to handle the basics of coding.
下一章将学习如何使用lambda表达式处理strings和比较对象。
In the next chapter we’ll cover how lambda expressions simplify another fundamental programming task: working with strings and comparing objects
英文
be poised to do = be prepared to do 有望做xxx
poised to (do something)
meaning : Braced, prepared, or ready to do something in the immediate future.
examples: We were all poised to work long hours to finish the project in time for the holidays.
The cobra reared its head up with its hood flared out, so I knew it was poised to strike.
https://idioms.thefreedictionary.com/poised+to+do
darn it!约等于damn it, 前者更正式。
beam me up. 美国俚语,让我离开这里,受够了的意思
学习与总结
mapToInt() 是 map() 的变种,把Stream变成特定的IntStream 流。
reduce() 方法,首先它也是把一个collection从头遍历到尾,每次比较两个元素,根据lambda表达式来返回一个结果,带着这个结果继续和后面的元素进行比较。最终的返回值呢?只有一个值。reduce比较安全,程序员自己不用担心遍历为空等情况。
reduce如果使用初始值的话,lambda表达式的第一个参数表示它。
join() 方法可以和collect() 方法连用。
之前翻看过这本书,主要目的是快速入门,方便使用java 的函数式编程做项目,当时的主要目的是用。如今是想系统地钻研这本书。
java functional programming一定要打好基础。 英文写作,打磨词句。
参考
Functional Programming in Java: Harnessing the Power Of Java 8 Lambda Expressions 1st Edition:https://www.amazon.com/Functional-Programming-Java-Harnessing-Expressions/dp/1937785467
source code: https://pragprog.com/titles/vsjava8/functional-programming-in-java/
|