- Functional Programming in Java venkat(3): Using Collections part2
- Introduction
- Using Collections
- Finding Elements
- Reusing Lambda Expressions
- Using Lexical Scoping and Closures
- 思考
- 参考
这里是记录学习这本书 Functional Programming in Java: Harnessing the Power Of Java 8 Lambda Expressions 的读书笔记,如有侵权,请联系删除。
About the author
Using Collections Finding ElementsVenkat 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),
filter()方法用来选取元素
The now-familiar elegant methods to traverse and transform collections will
not directly help pick elements from a collection. The filter() method is designed
for that purpose.
下面用老方法:选择出以N开始的名字。
From a list of names, let’s pick the ones that start with the letter N. Since
there may be zero matching names in the list, the result may be an empty
list. Let’s first code it using the old approach
final List<String> startsWithN = new ArrayList<String>();
for(String name : friends) {
if(name.startsWith("N")) {
startsWithN.add(name);
}
}
System.out.println(String.format("Found %d names", startsWithN.size()));
That’s a chatty piece of code for a simple task. We created a variable and
initialized it to an empty collection. Then we looped through the collection,
looking for a name that starts with the desired letter. If found, we added the
element to the collection; otherwise we skipped it.
现在使用filter方法来做
Let’s refactor this code to use the filter() method, and see how it changes things
final List<String> startsWithN =
friends.stream()
.filter(name -> name.startsWith("N"))
.collect(Collectors.toList());
System.out.println(String.format("Found %d names", startsWithN.size()));
filter方法需要一个返回boolean值的lambda表达式。如果执行到某个元素,lambda表达式返回true,就将这个元素加入到结果集合中。
后面我们使用collect方法把结果转换为List
The filter() method expects a lambda expression that returns a boolean result.
If the lambda expression returns a true, the element in context while executing
that lambda expression is added to a result collection; it’s skipped otherwise.
Finally the method returns a Stream with only elements for which the lambda
expression yielded a true. In the end we transformed the result into a List using
the collect() method—we’ll discuss this method further in Using the collect
Method and the Collectors Class, on page 52.
filter和map方法不同
map返回的结果长度和输入一样;filter结果的长度可以小于输入长度。
map可以返回其他类型的元素;filter必须返回输入集合的子集。
The filter() method returns an iterator just like the map() method does, but the
similarity ends there. Whereas the map() method returns a collection of the
same size as the input collection, the filter() method does not. It may yield a
result collection with a number of elements ranging from zero to the maximum
number of elements in the input collection. However, unlike map(), the elements
in the result collection that filter() returned are a subset of the elements in the
input collection.
lambda表达式的简洁不错,另外我们需要注意不要有code duplication
The conciseness we’ve achieved by using lambda expressions so far is nice,
but code duplication may sneak in quickly if we’re not careful. Let’s address
that concern next.
全部代码
package fpij;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Collectors;
import static fpij.Folks.friends;
public class PickElements {
public static void main(final String[] args) {
{
final List<String> startsWithN = new ArrayList<String>();
for(String name : friends) {
if(name.startsWith("N")) {
startsWithN.add(name);
}
}
System.out.println(String.format("Found %d names", startsWithN.size()));
}
System.out.println("//" + "START:FILTER_OUTPUT");
final List<String> startsWithN =
friends.stream()
.filter(name -> name.startsWith("N"))
.collect(Collectors.toList());
System.out.println(String.format("Found %d names", startsWithN.size()));
System.out.println("//" + "END:FILTER_OUTPUT");
}
}
Reusing Lambda Expressions
Lambda表达式很容易出现duplicate,这在软件设计中是bad smell
Lambda expressions are deceivingly concise and it’s easy to carelessly duplicate
them in code. Duplicate code leads to poor-quality code that’s hard to
maintain; if we needed to make a change, we’d have to find and touch the
relevant code in several places.
避免重复性的代码容易提高性能,只需要修改一小部分代码。
Avoiding duplication can also help improve performance. By keeping the code
related to a piece of knowledge concentrated in one place, we can easily study
its performance profile and make changes in one place to get better performance.
现在让我们看一下哪些lambda表达式容易duplicate的陷阱。
Now let’s see how easy it is to fall into the duplication trap when using
lambda expressions, and consider ways to avoid it.
这里我们有很多集合,里面都是姓名。需求是过滤出来某个字母开头的名字。
Suppose we have a few collections of names: friends, comrades, editors, and so
on. We want to filter out names that start with a certain letter.
package fpij;
import java.util.List;
import java.util.Arrays;
public class Folks {
public static final List<String> friends =
Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
public static final List<String> editors =
Arrays.asList("Brian", "Jackie", "John", "Mike");
public static final List<String> comrades =
Arrays.asList("Kate", "Ken", "Nick", "Paula", "Zach");
}
首先使用低级的手法
Let’s first take a naïve approach to this using the filter() method.
public static void main(final String[] args) {
{
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();
System.out.println(countFriendsStartN);
System.out.println(countComradesStartN);
System.out.println(countEditorsStartN);
}
上面的lambda表达式做的其实是同一件事,就是在重复,不满足DRY原则。如果一个地方更改,就要连带更改很多其他地方。
The lambda expressions made the code concise, but quietly led to duplicate
code. In the previous example, one change to the lambda expression needs
to change in more than one place—that’s a no-no. Fortunately, we can assign
lambda expressions to variables and reuse them, just like with objects.
下面是移除duplication的方法:我们把lambda表达式存起来,需要是Predicate返回值,然后把它传到各个方法中去。这样如果想要修改,只需要修改这一处逻辑即可。
Predicate函数接口的功能:主要是检查一个输入参数是否满足某些条件。主要的用途是作为Stream的方法的参数,比如作为filter方法的参数。
The filter() method, the receiver of the lambda expression in the previous
example, takes a reference to a java.util.function.Predicate functional interface.
Here, the Java compiler works its magic to synthesize an implementation of
the Predicate’s test() method from the given lambda expression. Rather than
asking Java to synthesize the method at the argument-definition location, we
can be more explicit. In this example, it’s possible to store the lambda
expression in an explicit reference of type Predicate and then pass it to the
function; this is an easy way to remove the duplication.
DRY principle,很重要哦。
Let’s refactor the previous code to make it DRY.1 (See the Don’t Repeat Yourself
—DRY—principle in The Pragmatic Programmer: From Journeyman to Master
[HT00], by Andy Hunt and Dave Thomas).
看下面的代码:我们把startWithB核心逻辑提取出来,下面是笔者自己的练习代码,其中原本是StartsWithN
{
final Predicate<String> startsWithB = name -> name.startsWith("B");
final long countFriendsStartB =
friends.stream()
.filter(startsWithB)
.count();
final long countEditorsStartB =
editors.stream()
.filter(startsWithB)
.count();
final long countComradesStartB =
comrades.stream()
.filter(startsWithB)
.count();
System.out.println("hello");
System.out.println(countFriendsStartB);
System.out.println(countComradesStartB);
System.out.println(countEditorsStartB);
}
这就是复用lambda表达式
Rather than duplicate the lambda expression several times, we created it once
and stored it in a reference named startsWithN of type Predicate. In the three calls
to the filter() method, the Java compiler happily took the lambda expression
stored in the variable under the guise of the Predicate instance.
还有一些其他问题
Using Lexical Scoping and ClosuresThe new variable gently removed the duplication that sneaked in. Unfortunately,
it’s about to sneak back in with a vengeance and we need something
a bit more powerful to thwart it, as we’ll see next.
lambda表达式带来重复和低质量的代码其实是误区,我们可以既享受lambda表达式的简洁,又可以不损失代码质量。
There’s a misconception among some developers that using lambda expressions
may introduce duplication and lower code quality. Contrary to that
belief, even when the code gets more complicated we still don’t need to compromise
code quality to enjoy the conciseness that lambda expressions give,
as we’ll see in this section.
上面我们复用lambda表达式的代码,如果我们想要查找起始字母为C的名字,又要写新的lambda表达式,这就带来了重复,看看我们如何处理。
We managed to reuse the lambda expression in the previous example; however,
duplication will sneak in quickly when we bring in another letter to match.
Let’s explore the problem further and then solve it using lexical scoping and
closures.
Duplication in Lambda Expressions
需求改变:我们现在同时选择以B和以N开头的名字
Let’s pick the names that start with N or B from the friends collection of names.
Continuing with the previous example, we may be tempted to write something
like the following:
下面的代码就很bad smell
{
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();
System.out.println(countFriendsStartN);
System.out.println(countFriendsStartB);
}
都是重复的代码
The first predicate tests if the name starts with an N and the second tests for
a B. We pass these two instances to the two calls to the filter() method,
respectively. Seems reasonable, but the two predicates are mere duplicates,
with only the letter they use being different. Let’s figure out a way to eliminate
this duplication.
Removing Duplication with Lexical Scoping
下面是解决方法,先创建一个Predicate类型的函数checkIfStartsWith
As a first option, we could extract the letter as a parameter to a function and
pass the function as an argument to the filter() method. That’s a reasonable
idea, but the filter() method will not accept some arbitrary function. It insists
on receiving a function that accepts one parameter representing the context
element in the collection, and returning a boolean result. It’s expecting a Predicate.
For comparison purposes we need a function that will cache the letter for
later use, and hold onto it until the parameter, name in this example, is
received. Let’s create such a function.
public static Predicate<String> checkIfStartsWith(final String letter) {
return name -> name.startsWith(letter);
}
这是一个高阶函数,可以被filter调用
We defined checkIfStartsWith() as a static function that takes a letter of type String
as a parameter. It then returns a Predicate that can be passed to the filter()
method for later evaluation. Unlike the higher-order functions we’ve seen so
far, which accepted functions as parameters, the checkIfStartsWith() returns a
function as a result. This is a higher-order function, as we discussed in Evolution,
Not Revolution, on page 12.
词法作用域lexical scoping(also called static scope) What is lexical scope?
lexical scoping定义了在嵌套函数中,变量名字是如何被解析的:内部函数会保留父函数(调用内部函数的函数)的作用域,即使父函数已经返回了。
现在的问题是letter是依附于哪个变量的?显然不在这个匿名函数中,java会跳出这个范围,然后去查找变量letter。
Lexical scoping is a powerful technique that lets us cache values provided in one context for use later in another context.
The Predicate that checkIfStartsWith() returned is different from the lambda
expressions we’ve seen so far. In returnname -> name.startsWith(letter)
, it’s clear
what name is: it’s the parameter passed to this lambda expression. But what’s
the variable letter bound to? Since that’s not in the scope of this anonymous
function, Java reaches over to the scope of the definition of this lambda
expression and finds the variable letter in that scope. This is called lexical
scoping. Lexical scoping is a powerful technique that lets us cache values
provided in one context for use later in another context. Since this lambda
expression closes over the scope of its definition, it’s also referred to as a closure.
For lexical scope access restriction, see Are there restrictions to lexical
scoping?, on page 30.
Joe asks:
Are there restrictions to lexical scoping? 词法作用域有什么限制嘛?
lambda表达式内部只能使用final变量或者是 in the enclosing scope中effectively final。
effectively final的意思是在这个变量初始化之后不去修改它就称为effectively final。
enclosing scope中的遍历不能是可变的,防止(比如并行时的)资源竞争。
What is an enclosing scope?
From within a lambda expression we can only access local variables that are final or
effectively final in the enclosing scope.A lambda expression may be invoked right away, or it may be invoked lazily or from
multiple threads. To avoid race conditions, the local variables we access in the enclosing scope are not allowed to change once initialized. Any attempt to change
them will result in a compilation error.Variables marked final directly fit this bill, but Java does not insist that we mark them
as such. Instead, Java looks for two things. First, the accessed variables have to be
initialized within the enclosing methods before the lambda expression is defined.
Second, the values of these variables don’t change anywhere else—that is, they’re
effectively final although they are not marked as such.When using lambda expressions that capture local state, we should be aware that
stateless lambda expressions are runtime constants, but those that capture local
state have an additional evaluation cost.
现在我们看看使用checkIfStartsWith方法,将其作为filter的参数使用
We can use the lambda expression returned by checkIfStartsWith() in the call to
the filter() method, like so:
{
final long countFriendsStartN =
friends.stream()
.filter(checkIfStartsWith("N")).count();
final long countFriendsStartB =
friends.stream()
.filter(checkIfStartsWith("B")).count();
System.out.println(countFriendsStartN);
System.out.println(countFriendsStartB);
}
In the calls to the filter() method, we first invoke the checkIfStartsWith() method,
passing in a desired letter. This call immediately returns a lambda expression
that is then passed on to the filter() method.
通过使用checkIfStartsWith这个高阶函数,并且使用lexical scoping,把重复的代码移除掉了。
By creating a higher-order function, checkIfStartsWith() in this example, and using
lexical scoping, we managed to remove the duplication in code. We did not
have to repeat the comparison to check if the name starts with different letters.
Refactoring to Narrow the Scope
尽管上面的代码已经去除了一定的重复,但是还有一点就是使用的是静态方法,会对整个类造成影响。现在的想法是来限制方法的scope作用域。不让它对整个类产生危害。
In the preceding (smelly) example we used a static method, but we don’t want
to pollute the class with static methods to cache each variable in the future.
It would be nice to narrow the function’s scope to where it’s needed. We can
do that using a Function class.
使用Function类: 输入的是String类型,返回的是Predicate的lambda表达式
final Function<String, Predicate<String>> startsWithLetter =
(String letter) -> {
Predicate<String> checkStarts = (String name) -> name.startsWith(letter);
return checkStarts;
};
这个lambda表达式代替上面的checkIfStartsWith()这个静态方法。
This lambda expression replaces the static method checkIfStartsWith() and can
appear within a function, just before it’s needed. The startsWithLetter variable
refers to a Function that takes in a String and returns a Predicate.
下面就是对上面的这个Function lambda表达式进行优化,去除一些verbose的地方。
This version is verbose compared to the static method we saw earlier, but we’ll
refactor that soon to make it concise. For all practical purposes, this function
is equivalent to the static method; it takes a String and returns a Predicate. Instead
of explicitly creating the instance of the Predicate and returning it, we can
replace it with a lambda expression.
final Function<String, Predicate<String>> startsWithLetter =
(String letter) -> (String name) -> name.startsWith(letter);
这里的推导过程,笔者进行了自己的补充:先从上面的return一个lambda表达式(中间变量),这里把中间变量省去,变成如下的样子
// 补充
final Function<String, Predicate<String>> startsWithLetter =
(String letter) -> {
return (String name) -> name.startsWith(letter);
};
然后又由于{}中只有返回值,所以又可以进行优化,省略掉{} 和return,得到如下:
final Function<String, Predicate<String>> startWithLetter =
(String letter) -> (String name) -> name.startWith(letter);
具体应用
{
final Function<String, Predicate<String>> startsWithLetter =
(String letter) -> (String name) -> name.startsWith(letter);
final long countFriendsStartN =
friends.stream()
.filter(startsWithLetter.apply("N")).count();
final long countFriendsStartB =
friends.stream()
.filter(startsWithLetter.apply("B")).count();
System.out.println(countFriendsStartN);
System.out.println(countFriendsStartB);
}
继续优化:去掉变量的类型和括号,让java编译器自动推导
We reduced clutter, but we can take the conciseness up another notch by
removing the types and letting the Java compiler infer the types based on the
context. Let’s look at the concise version.
final Function<String, Predicate<String>> startsWithLetter =
letter -> name -> name.startsWith(letter);
我们要逐渐熟悉lambda简洁的语法。
It takes a bit of effort to get used to this concise syntax. Feel free to look away
for a moment if this makes you cross-eyed. Now that we’ve refactored that
version, we can use it in place of the checkIfStartsWith(), like so:
目前做好的版本啦
{
final Function<String, Predicate<String>> startsWithLetter =
letter -> name -> name.startsWith(letter);
final long countFriendsStartN =
friends.stream()
.filter(startsWithLetter.apply("N")).count();
final long countFriendsStartB =
friends.stream()
.filter(startsWithLetter.apply("B")).count();
System.out.println(countFriendsStartN);
System.out.println(countFriendsStartB);
}
这一章我们我们学习了如何传递函数给另一个函数,在函数内部创建函数,从函数中返回一个函数。同时,我们也学习了lambda表达式的简洁和复用。
We’ve come full circle with higher-order functions in this section. Our examples
illustrate how to pass functions to functions, create functions within functions,
and return functions from within functions. They also demonstrate the conciseness
and reusability that lambda expressions facilitate.
下面介绍一下Function和Predicate的区别
如果我们想要把transform输入序列到其他值,我们多使用Function,同时它也多与map一起使用,两者都是做的transform的工作。
We made good use of both Function and Predicate in this section, but let’s discuss
how they’re different. A Predicate takes in one parameter of type T and
returns a boolean result to indicate a decision for whatever check it represents.
We can use it anytime we want to make a go or no-go decision for a candidate
we pass to the predicate. Methods like filter() that evaluate candidate elements
take in a Predicate as their parameters. On the other hand, a Functionrepresents
a function that takes a parameter of type T and returns a result of
type R. This is more general than a Predicate that always returns a boolean. We can use a Function anywhere we want to transform an input to another value,
so it’s quite logical that the map() method uses it as its parameter.
下一节学习从一个collection中选择出来一个元素。
思考Selecting elements from a collection was easy. Next we’ll cover how to pick
just one element out of a collection.
这次笔记学习到filter,自然而言想到和map做对比。加深对各个方法的理解。
map | filter |
---|---|
map返回的结果长度和输入一样 | filter结果的长度可以小于输入长度 |
map可以返回其他类型的元素 | filter必须返回输入集合的子集 |
同时对Java中的Lexical Scoping(语法作用域) 和 Closure(闭包) 有了进一步的了解。
任何高手都是一本书一本书啃出来的。自己也要跟上!
参考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/
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)