流Stream
1、外部迭代到内部迭代。
求总数的代码:
int count = 0;
for(Artist artist : allArtists){
if(artist.isForm("Lodon")){
count++;
}
}
这样的操作存在几个问题:
1、每次迭代集合元素都需要写很多样板代码
2、将for循环改成并行很麻烦
3、for循环过大不一定能很好传达程序员意图
forEach是一个语法糖,实际操作如下:
int count = 0;
Iterator<Artist> iterator = allArtist.iterator();
while(iterator.hasNext()){
Artist artist = iterator.next();
if(artist.isFrom("Lodon")){
count++;
}
}
内部迭代操作:
long count = allArtist.stream()
.filter(artist -> artist.isFrom("London"))
.count();
2、实现机制
在上面的例子,过程分解为两个简单的操作:过滤和计数。
两次操作是否需要两次循环?实际上,只需要迭代一次。
java中通常调用一个方法,计算机会立即执行操作。Stream里面的一些方法却有些不同,虽然是普通的java方法,但返回的Stream对象却不是一个新集合,而是创建新集合的配方。
allArtist.stream()
.filter(artist -> artist.isForm("London"))
这代码其实并没有做什么实质工作,只是刻画出了stream,但没有产生新的集合。类似filter这样只描述filter,最终不产生新集合的方法叫做惰性求值方法。类似count这样最终从Stream产生值的方法叫做及早求值方法。
3、常用的流操作
collect(toList()):由Stream里面的值生成一个列表。
of方法适用一组初始值生成新的strean
public void tranList(){
Stream<String> stream = Stream.of("a","b","ab","c");
List<String> list = stream.collect(Collectors.toList());
}
map
如果有一个函数可以将一种类型的值转换成另一种类型,map操作就可以使用该函数,将一个流中的值转换成另一个流。
public void testMap(){
/** 原操作 **/
List<String> collected = new ArrayList<>();
for(String string : Arrays.asList("a","b","hello")){
String upperCaseString = string.toUpperCase();
collected.add(upperCaseString);
}
/** 现操作 **/
List<String> collected1 = Stream.of("a", "b", "ab")
.map(string -> string.toUpperCase())
.collect(Collectors.toList());
}
传给例子中的传给map的Lambda表达式只接受一个String类型的参数,返回一个新的String。参数和返回值不必属于同一种类型,但是Lambda表达式必须是Function接口的一个实例。接受T,返回R
filter
遍历并检查其中的元素时,可以尝试使用stream中提供的新方法filter
public void testFilter(){
List<String> beginningWithNumbers = Stream.of("a", "1abc", "abc1")
.filter(value -> Character.isDigit(value.charAt(0)))
.collect(Collectors.toList());
}
和map一样,filter接受一个函数作为参数,函数用Lambda表示。函数返回boolean,返回true的函数被保留。该Lambda表达式就是Predicate
flatMap
flatMap方法可以为Stream替换值,然后将多个Stream连接成一个Stream
public void testFlatMap(){
List<Integer> together = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(Collectors.toList());
}
调用stream方法,将每个列表转换成stream对象,其余部分由flatMap方法处理。
flatMap方法的相关函数接口和map方法的一样,都是Function接口,只是方法的返回值限定为stream类罢了。
max和min
Stream上常用的操作之一是求最大值和最小值。Stream API中的max和min操作足以解决这一问题。
public void testMinAndMax(){
List<Track> tracks = Arrays.asList(new Track("Bakai", 524),
new Track("Violets for Your Furs", 378),
new Track("Time Was", 451));
Track shortTestTrack = tracks.stream()
.min(Comparator.comparing(track -> track.getName().length()))
.get();
}
通用模式
max和min方法都属于更通用的一种编程模式。要看到这种编程模式,最简单的方法是使用for循环重写上面例子中的代码。
public void usuallyModel(){
List<Track> tracks = Arrays.asList(new Track("Bakai", 524),
new Track("Violets for Your Furs", 378),
new Track("Time Was", 451));
Track sortTestTrack = tracks.get(0);
for(Track track: tracks){
if(track.getName().length() < sortTestTrack.getName().length()){
sortTestTrack = track;
}
}
}
先使用第一个元素初始化一个变量,然后遍历列表,如果找到更适合的,则更新外面的变量,最后找到的就是最适合的变量
Stream API中的reduce可以达到。
reduce
reduce操作可以实现从一组值中生成一个值。上面例子中的count、min、max,因为过于常用被纳入标准库。其实,这些方法都是reduce操作。
public void testReduce(){
int count = Stream.of(1, 2, 3)
.reduce(0, (acc, element) -> acc + element);
}
Lambda表达式的返回值是最新的acc,是上一轮acc的值和当前元素相加的结果。reduce类型是BinaryOperator类型。
将reduce代码展开,就是下面的代码:
BinaryOperator<Integer> accumulator = (acc, element) -> acc + element;
int count1 = accumulator.apply(
accumulator.apply(accumulator.apply(0, 1),
2),
3);
原来的命令编程写法:
int acc = 0;
for(Integer element : Arrays.asList(1, 2, 3)){
acc = acc + element;
}
整合操作
Stream接口的方法如此之多,有时会让人难以选择,不知道使用什么更好。
例子:
需求:找出某张专辑上所有乐队的国籍。艺术家列表里既有个人,也有乐队。利用一点领域知识,假定一般乐队名以定冠词The开头。
步骤:
1、找出专辑上的所有表演者
2、分辨出那些表演者是乐队
3、找出每个乐队的国籍
4、将找出的国籍放入一个集合
performers.stream()
.filter(performer -> performer.getName().startWith("The"))
.map(performer - performer.getCountry())
.collect(Collection.toSet());
一个思考,操作的时候,你真的需要暴露整个List或Set这样的集合对象吗?可能一个Stream工厂才是更好的选择。通过Stream暴露集合的最大优点在于,它很好的封装了内部的数据结构。仅暴露一个Stream接口,用户在实际操作中无论如何使用,都不会影响内部的List或Set。
4、重构遗留代码
如何将一段使用循环进行集合操作的代码,重构成基于Stream的操作。
需求:
选定一组专辑,找出其中所有长度大于1分钟的曲目名称。
遗留代码:
public Set<String> findLongTracks(List<Album> albums){
Set<String> trackNames = new HashSet<>();
for(Album album : albums){
for(Track track :album.getTrackList()){
if(track.getLength() > 60){
String name = track.getName();
trackNames.add(name);
}
}
}
return trackNames;
}
代码分析:
1、声明外部装载容器
2、遍历专辑
3、遍历专辑中的曲目
4、找出专辑中曲目长度大于60的
5、将长度大于60的装进外部集合
Stream API实现:
1、专辑得到流Stream
2、流操作结束得到一个集合。reduce操作。
3、reduce操作分解:
输入空集合A,流B 得到 新集合A
子reduce操作:
filter () 筛选流
map 映射替换得到名字
合并集合
代码实现:
/**
Set<String> names = new HashSet<String>();
albums.stream()
.reduce(names,album -> {
album.getTrackList().stream()
.filter(track -> track.getLength() > 60)
.map(trace -> trace.getName())
.reduce(names,name -> names.add(name))
})
.collect(Collection.toSet());
前面的代码是错误的,因为我忽略了一个问题,匿名函数里面的外部成员必须是final
**/
albums.stream()
.flatMap(album -> album.getTracks())
.stream()
.filter(track -> track.getLength() > 60)
.map(trace -> trace.getName())
.collect(Collection.toSet());
5、多次调用流操作。
用户也可以选择每一步强制对函数求值,而不是将所有的方法调用链接在一起,但是最好不要如此操作。
List<Artist> musiciams = album.getMusicians().collect(toList());
List<Artist> bands = musicans.stream()
.filter(artist -> artist.getName().startsWith("The"))
.collect(toList());
Set<String> origins = binds.stream()
.map(artist -> artist.getNationality())
.collect(toSet());
符合Stream使用习惯的链式调用:
Set<String> origins = alubm.getMusicans()
.filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getNationality())
.collect(toSet());
多次调用相比链式调用的缺点:
1、代码可读性茶,隐藏真正业务逻辑
2、效率差,每一步都要对流及早求值,生成新的集合
3、代码充斥一堆垃圾变量
3、难于自动并行化处理
6、高阶函数
本章中不断出现被函数式编程程序员称为高阶函数的操作。高阶函数式指能接受另一个函数作为参数,或返回一个函数的函数。
map是一个高阶函数。实际上,本章介绍的Stream接口中几乎所有的函数都是高阶函数。之前的排序例子中用到了comparing函数,它接受一个函数作为参数,获取对应值,同时返回一个Comparator。Comparator可能被误认为是一个对象,但它有且只有一个抽象方法,所以实际上时一个函数接口。
7、正确使用Lambda表达式
开始介绍Lambda表达式时,能输出一些信息的回调函数为示例。回调函数式一个合法的Lambda表达式,但并不能真正帮助用户写出更简单、更抽象的代码,因为它仍然在只会计算机执行一个操作。
本章的介绍能让用户写出更简单的代码,因为这些概念描述了数据上的操作,明确了要达成什么转化,而不是说明如何转化,这样子写出的代码,潜在的缺陷更少。
没有副作用的函数不会改变程序或外界的状态。书中的第一个Lambda表达式是由副作用的,它向控制台输出了信息————一个可观测到的副作用。无论何时,将Lambda表达式传给Stream上的高阶函数,都应该尽量避免副作用。唯一的例外是forEach方法,它是一个终结方法。