Java函数式编程(四):在集合中查找元素
查找元素
现在我们对这个设计优雅的转化集合的方法已经不陌生了,但它对查找元素却也是无能为力。不过filter方法却是为这个而生的。
我们现在要从一个名字列表中,取出那些以n开头的名字。当然可能一个也没有,结果可能是个空集合。我们先用老方法实现一把。
final list<string> startswithn = new arraylist<string>();
for(string name : friends) {
if(name.startswith("n")) {
startswithn.add(name);
}
}
这么简单的事件,写了这么多代码,也够啰嗦的了。我们先创建了一个变量,然后把它初始为一个空集合。然后遍历原来的集合,查找那些以指定字母开头的名字。如果找到,就插入到集合里。
我们用filter方法来重构一下上面这段代码,看看它的威力到底如何。
final list<string> startswithn =
friends.stream()
.filter(name -> name.startswith("n"))
.collect(collectors.tolist());
filter方法接收一个返回布尔值的lambda表达式。如果表达式结果为true,运行上下文中的那个元素就会被添加到结果集中;如果不是,就跳过它。最终返回的是一个steam,它里面只包含那些表达式返回true的元素。最后我们用一个collect方法把这个集合转化成一个列表——在后面52页的使用collect方法和collecters类中,我们会对这个方法进去更深入的探讨。
我们来打印一下这个结果集中的元素:
system.out.println(string.format("found %d names", startswithn.size()));
从输出结果很明显能看出来,这个方法把集合中匹配的元素全都找出来了。
found 2 names
filter方法和map方法一样,也返回了一个迭代器,不过它们也就这点相同而已了。map返回的集合和输入集合大小是一样的,而filter返回的可不好说。它返回的集合的大小区间,从0一直到输入集的元素个数。和map不一样的是,filter返回的是输入集的一个子集。
到现在为止,lambda表达式带来的代码简洁性让我们很满意,不过如果不注意的话,代码冗余的问题就开始慢慢滋长了。下面我们来讨论下这个问题。
lambda表达式的重用
lambda表达式看起来很简洁,实际上一不小心很容易就出现代码冗余了。冗余会导致代码质量低下,难以维护;如果我们想做一个改动,得把好几处相关的代码都一起改掉才行。
避免冗余还可以帮忙我们提升性能。相关的代码都集中在一个地方,这样我们分析它的性能表现,然后优化这一处的代码,很容易就能提升代码的性能。
现在我们来看下为什么使用lambda表达式容易导致代码冗余,同时考虑如何去避免它。
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.
我们希望过滤一下某个字母开头的名字。先用filter方法简单地实现一下。
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表达式让代码看起来很简洁,不过它不知不觉的带来了代码的冗余。在上面这个例子中,如果想改一下lambda表达式,我们得改不止一处地方——这可不行。幸运的是,我们可以把lambda表达式赋值给变量,然后对它们进行重用,就像使用对象一样。
filter方法,lambda表达式的接收方,接收的是一个java.util.function.predicate函数式接口的引用。在这里,java编译器又派上用场了,它用指定的lambda表达式生成了predicate的test方法的一个实现。现在我们可以更明确的让java编译器去生成这个方法,而不是在参数定义的地方再生成。在上面例子中,我们可以明确的把lambda表达式存储到一个predicate类型的引用里面,然后再把这个引用传递给filter方法;这样做很容易就避免了代码冗余。
我们来重构前面这段代码,让它符合dry的原则吧。(don't repeat yoursef——dry——原则,可以参看the pragmatic programmer: from journeyman to master[ht00],一书)。
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();
现在不用再重复写那个lambda表达式了,我们只写了一次,并把它存储到了一个叫startswithn的predicate类型的引用里面。这后面的三个filter调用里,java编译器看到在predicate伪装下的lambda表达式,笑而不语,默默接收了。
这个新引入的变量为我们消除了代码冗余。不过不幸的是,后面我们就会看到,敌人很快又回来报仇雪恨了,我们再看看有什么更厉害的武器能替我们消灭它们。