(转)Java8使⽤lambda表达式进⾏集合的遍历
本⽂转⾃
我们经常会⽤到各种集合,数字的,字符串的还有对象的。它们⽆处不在,哪怕操作集合的代码要能稍微优化⼀点,都能让代码清晰很多。在这章中,我们探索下如何使⽤lambda表达式来操作集合。我们⽤它来遍历集合,把集合转化成新的集合,从集合中删除元素,把集合进⾏合并。
遍历列表
遍历列表是最基本的⼀个集合操作,这么多年来,它的操作也发⽣了⼀些变化。我们使⽤⼀个遍历名字的⼩例⼦,从最古⽼的版本介绍到现在最优雅的版本。
lambda编程⽤下⾯的代码我们很容易创建⼀个不可变的名字的列表:
final List<String> friends =
Arrays.asList("Brian","Nate","Neal","Raju","Sara","Scott");
System.out.(i));
}
下⾯这是最常见的⼀种遍历列表并打印的⽅法,虽然也最⼀般:
for(int i =0; i < friends.size(); i++){
System.out.(i));
}
我把这种⽅式叫做⾃虐型写法——⼜啰嗦⼜容易出错。我们得停下来好好想想,"是i<;还是i<=呢?"这只有当我们需要操作具体某个元素的时候才有意义,不过即便这样,我们还可以使⽤坚持不可变原则的函数式风格来实现,这个我们很快会讨论到。
Java还提供了⼀种相对先进的for结构。
collections/fpij/Iteration.java
for(String name : friends){
System.out.println(name);
}
在底层,这种⽅式的迭代是使⽤Iterator接⼝来实现的,调⽤了它的hasNext和next⽅法。 这两种⽅式都属于外部迭代器,它们把如何做和想做什么揉到了⼀起。我们显式的控制迭代,告诉它从哪开始到哪结束;第⼆个版本则在底层通过Iterator的⽅法来做这些。显式的操作下,还可以⽤break和continue语句来控制迭代。 第⼆个版本⽐第⼀个少了点东西。如果我们不打算修改集合的某个元素的话,它的⽅式⽐第⼀个要好。不过这两种⽅式都是命令式的,在现在的Java中应该摒弃这种⽅式。 改成函数式原因有这⼏个:
for循环本⾝是串⾏的,很难进⾏并⾏化。
这样的循环是⾮多态的;所得即所求。我们直接把集合传给for循环,⽽不是在集合上调⽤⼀个⽅法(⽀持多态)来执⾏特定的操作。
从设计层⾯来说,这样 写的代码违反了“Tell,Don't Ask”的原则 。我们请求执⾏⼀次迭代,⽽不是把迭代留给底层库来执⾏。
是时候从⽼的命令式编程转换到更优雅的内部迭代器的函数式编程了。使⽤内部迭代器后我们把很多具体操作都扔给了底层⽅法库来执⾏,你可以更专注于具体的业务需求。底层的函数会负责进⾏迭代的。我们先⽤⼀个内部迭代器来枚举⼀下名字列表。
Iterable接⼝在JDK8中得到加强,它有⼀个专门的名字叫forEach,它接收⼀个Comsumer类型的参数。如名字所说,Consumer的实例正是通过它的accept⽅法消费传递给它的对象的。我们⽤⼀个很熟悉的匿名内部类的语法来使⽤下这个forEach⽅法:
friends.forEach(new Consumer<String>(){public void accept(final String name){
System.out.println(name);}
});
我们调⽤了friends集合上的forEach⽅法,给它传递了⼀个Consumer的匿名实现。这个forEach⽅法从对集合中的每⼀个元素调⽤传⼊的Consumer的accept⽅法,让它来处理这个元素。在这个⽰例中我们只是打印了⼀下它的值,也就是这个名字。 我们来看下这个版本的输出结果,和上两个的结果 是⼀样的:
Brian
Nate
Neal
Raju
Sara
Scott
我们只改了⼀个地⽅:我们抛弃了过时的 for循环,使⽤了新的内部迭代器。好处是,我们不⽤指定如何迭代这个集合,可以更专注于如何处理每⼀个元素。缺点是,代码看起来更啰嗦了——这简直要把新的编码风格带来的喜悦冲的⼀⼲⼆净了。所幸的是,这个很容易改掉,这正是lambda表达式和新的编译器的威⼒⼤展⾝⼿的时候了。我们再做⼀点修改,把匿名内部类换成lambda表达式。
friends.forEach((final String name)-> System.out.println(name));
这样看起来就好多了。代码更少了,不过我们先来看下这是什么意思。这个forEach⽅法是⼀个⾼阶函数,它接收⼀个lambda表达式或者代码块,来对列表中的元素进⾏操作。在每次调⽤的时候 ,集合中的元素会绑定到name这个变量上。底层库托管了lambda表达式调⽤的活。它可以决定延迟表达式的执⾏,如果合适的话还可以进⾏并⾏计算。 这个版本的输出也和前⾯的⼀样。
Brian
Nate
Neal
Raju
Sara
Scott
内部迭代器的版本更为简洁。⽽且,使⽤它的话我们可以更专注每个元素的处理操作,⽽不是怎么去遍历——这可是声明式的。
不过这个版本还有缺陷。⼀旦forEach⽅法开始执⾏了,不像别的两个版本,我们没法跳出这个迭代。(当然有别的⽅法能搞定这个)。因此,这种写法在需要对集合⾥的每个元素处理的时候⽐较常⽤。后⾯我们会介绍到⼀些别的函数可以让我们控制循环的过程。
lambda表达式的标准语法,是把参数放到()⾥⾯,提供类型信息并使⽤逗号分隔参数。Java编译器为了解放我们,还能⾃动进⾏类型推导。不写类型当然更⽅便了,⼯作少了,世界也清静了。下⾯是上⼀个版本去掉了参数类型之后的:
friends.forEach((name)-> System.out.println(name));
在这个例⼦⾥,Java编译器通过上下⽂分析,知道name的类型是String。它查看被调⽤⽅法forEach的签名,然后分析参数⾥的这个函数式接⼝。接着它会分析这个接⼝⾥的抽象⽅法,查看参数的个数及类型。即便这个lambda表达式接收多个参数,我们也⼀样能进⾏类型推导,不过这样的话所有参数都不能带参数类型;在lambda表达式中,参数类型要么全不写,要写的话就得全写。
Java编译器对单个参数的lambda表达式会进⾏特殊处理:如果你想进⾏类型推导的话,参数两边的括号可以省略掉。
friends.forEach(name -> System.out.println(name));
这⾥有⼀点⼩警告:进⾏类型推导的参数不是final类型的。在前⾯显式声明类型例⼦中,我们同时也把参数标记为final的。这样能防⽌你在lambda表达式中修改参数的值。通常来说,修改参数的值是个坏习惯,这样容易引起BUG,因此标记成final是个好习惯。不幸的是,如果我们想使⽤类型推导的话,我们就得⾃⼰遵守规则不要修改参数,因为编译器可不再为我们保驾护航了。
⾛到这步可费了⽼劲了,现在代码量确实少了⼀点。不过这还不算最简。我们来体验下最后这个极简版的。
friends.forEach(System.out::println);
在上⾯的代码中我们⽤到了⼀个⽅法引⽤。我们⽤⽅法名就可以直接替换整个的代码了。在下节中我们会深⼊探讨下这个,不过现在我们先来回忆下Antoine de Saint-Exupéry的⼀句名⾔:完美不是⽆法再增添加什么,⽽是⽆法再去掉什么。
lambda表达式让我们能够简洁明了的进⾏集合的遍历。下⼀节我们会讲到它如何使我们在进⾏删除操作和集合转化的时候,也能够写出如此简洁的代码。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。