Java8 stream入门

無名 发表于: 2016-06-25   最后更新时间: 2016-06-25 11:30:04  
{{totalSubscript}} 订阅, 5,276 游览

Java8 stream入门

java8的变化是为了帮助我们编写更好的代码,新的核心代码库作为这种变化中的一部分,我们这章开始讨论新的库,变化最主要集中在集合api以及新添加的特性streams.streams让我们在更高的抽象层上处理集合。

从外部迭代到内部迭代:
java中对于集合最常最的处理就是迭代,处理集合中的每个元素。

int count = 0;
for(Artist artist : allArtists){
  if(artist.isFrom("Landon")){
  count++;
 }
}

这种方法有些缺陷,有很多无用的代码,同时编写一个并行的循环也特别的困难。

程序员通过这种写法不能很好的表达自己的思想。这种模板式代码掩盖了代码本身的含义,如果仅仅是一个单独的循环并没有什么,但是如果有大量类似的代码存在,那么就会成为负担。

让我们深入一些,看看掩藏在这种语法糖之下的操作,首先调用iterator方法,该方法调用一个iterator对象控制迭代的过程,我们称之为外部递归。接下来递归的具体操作就是调用hasNext以及next方法。

intcount = 0;
Iterator<Artist> iterator = allArtists.iterator();
  while(iterator.hasNext()){
  Artist artist = iterator.next();
  if (artist.isFrom("London")) {
  count++;
 }
}

外部递归有几个不足的地方:

首先:很难抽象出不同的操作,我们稍后会遇到这种情况。在循环中如何做和做什么的冲突。
另外一种方法是内部迭代,如下所示,我们首先调用stream()方法,该方法扮演的角色同iterator方法类似,不同于iterator方法返回一个Iterator对象控制迭代,stream方法返回内部迭代世界中的对象Stream.

long count = allArtists.stream().filter(artist -> artist.isFrom("landom")).count();

我们可以把该例子分为两步:

  1. 获取artists中所有来自london的artist

  2. 统计数量

每步操作对应着Stream接口中的一个方法,我们过滤Stream来查找来自london的artist,过滤意味着只获取符合要求的对象,因为我们只是调用Streams的api进行操作,并没有改变集合的内容,count()方法统计stream中的对象数量。

到底发生了什么?

当我编写上面的例子的时候,我将操作分为两步:过滤盒技术,你可能认为这个太费事了,之前的遍历例子只有一个循环,而这里似乎需要两个,事实上聪明的编译器只会遍历一个集合。

在java中,当你调用一个方法的时候相当于让计算机做一些操作,例如:System.out.println("hello world"),这条语句在终端打印一条语句,Stream中的某些方法有些不一样,他们也是普通的方法,但是Stream对象返回的不是一个新的集合,而是创建一个新集合的配方,因此在考虑一下上面的代码做了什么操作?

事实上,它并没有做那么多,filter方法的调用创建了一个新集合的配方,但是并不强制要求执行,而是延迟执行,之后的count()方法的调用才去执行,count这种方法称之为eager。
如下代码中,我们加入打印语句,但是执行的时候并没有打印任何内容,从中我们可以体会这种延迟执行的操作。

allArtist.stream().filter(artist ->{
    System.out.print(artist.getName());
    return artist.isFrom("landon");
})

如果我们加上操作count()方法那么可以发现终端上会有打印。

allArtist.stream().filter(artist ->{
System.out.print(artist.getName());
return artist.isFrom("landon");
}).count();

区分eager和lazy很简单,看他们的返回,如果返回Stream那么该方法是lazy,否则为eager。这种区别非常有意义,通常的用法是一串的lazy方法后面跟一个eager方法来生成结果。
这种做法类似于建造者模式,在建造者模式中定义了一系列的方法来组建配置对象,而后调用一个构建方法,对象在最后调用构建方法才会被构建出来。
我确定你会问:为什么我们要区分lazy方法和eager方法呢?我们了解更多操作极其结果之后,我们可以更有效的计算,我们可以串联起来很多对于集合的操作而仅仅只遍历一遍集合。

普通的Stream操作?

这里我们回顾一下Stream的操作以便更好的运用这些API,这里只列举了其中一些很关键的方法,我建议去看看关于这些新api的javadoc。

collect(toList())

该方法是eager方法,从Stream中生成一个新的队列。Stream中的值来自于原始的集合以及一系列的Stream调用。

List<String> collected = Stream.of("a", "b", "c").collect(Collectors.toList());

上例展示了collect(toList())从一个Stream中构建一个列表,有一点很重要,需要记住,在之前的章节中讨论过,因为很多Stream的方法是lazy的,因此在需要最后调用一个eager方法。

map

如果你有一个字符串列表要转换成为大小,你需要遍历所有的字符串然后调用toUppercase方法,然后将这些新值放入到一个新的列表中,如下:

List<String> collected = new ArrayList<>();
for(String string : asList("a","b", "hello")){
String upperCase = string.toUpperCase();
collected.add(uppercaseString);
}

map是Stream中最常见的一种操作

List<String> collected = Stream.of("a", "b", "hello").map(string -> string.toUpperCase()).collect(toList());

lambda表达式被传入到一个map方法中,接受一个String类型参数并返回一个String。

filter

我们之前已经看过filter的示例

flatMap

通过flatMap,可以使用一个Stream替换某个值,然后把所有的Stream合并起来。
我们已经看到map操作,将值替换一个新的值,但是有时候我们想替换为一个新的Stream对象,更常见的是把多个Stream和合并为一个Stream.

List<Integer> together = Stream.of(asList(1, 2),asList(3, 4)).flatMap(numbers -> numbers.stream()).collect(toList());

max和min

某些时候我们需要找出集合中最大或者最小的元素,幸运的是Stream api提供了max和min.

List<Track> tracks = asList(new Track("Bakai", 524),
new Track("Violets for Your Furs", 378),
new Track("Time Was", 451));
Track shortestTrack = tracks.stream()
.min(Comparator.comparing(track -> track.getLength()))
.get();

我们讨论最大以及最小的元素,第一件需要考虑的事情就是我们使用的排序方式,这里使用歌曲的长度作为排序方式。

我们传入一个Comparator到Stream中来进行排序,java8在Comparator中添加了一个静态方法来创建一个comparator对象,以前我们必须写非常丑陋的代码,获取两个对象中的某个属性然后进行比较,现在为了我们只需为进行比较的属性值添加set,get方法即可。

这种比较方式值得我们思考一下,其实是一个函数接受一个函数,并且返回一个函数,写法漂亮且有用,在过去的时候,内部类的写法可读性差,现在lambda表达式方便而且简洁。

现在可以在一个空的Stream上面调用max,返回一个可选的结果,可选的意思是这个值可能存在也可能不存在,如果Stream是空的,那么就不存在,如果Stream不为空,那么最大值就存在,现在我们不考虑值的可选性,稍后再考虑,唯一需要记住的就是我们可以通过get方法获取结果值。

一种常规模式:

max以及min都是常见的编写代码的模式
下面通过循环重写的代码

List<Track> tracks = asList(new Track("Bakai", 524),
    new Track("Violets for Your Furs", 378),
    new Track("Time Was", 451));
Track shortestTrack = tracks.get(0);
    for (Track track : tracks) {
        if (track.getLength() < shortestTrack.getLength()) {
        shortestTrack = track;
    }
}

上面的代码最开始用列表中第一个值初始化shortestTrack变量的值,然后便利tracks,如果遇到更短的track,那么替换shortestTack的值,最后shortestTrack的值就是最小的值。毫无疑问,在你的编码生涯中会有很多这种循环,他们中的大部分都遵循这种模式,下面是伪代码。

Object accumulator = initialValue;
for(Object element : collection) {
    accumulator = combine(accumulator, element);
}

一个accumulator贯穿循环,我们试图计算accumulator的最终值。accumulator有一个初始值,然后和列表中的每个元素进行运算。

reduce

当你需要从一个集合中提炼出一个值的时候,我们使用reduce方法,在先前的例子中,我们使用count,min和max方法,这些方法在标准库中都有,所有这些方法都是reduce的一种形式。
让我们演示一下叠加streams中的所有元素

int count = Stream.of(1,2,3).reduce(0,(acc,element) -> acc + element);

lambda表达式将当前的acc值和当前的值叠加返回作为新的acc的值。

Putting Operations Together

Stream接口中有如此之多的方法,让我们好像在迷宫中寻找自己所需的方法一样,因此让我们通过一个例子让我们如何分解Stream的操作流程。

首先需要确定的就是,解决方案不是一个个单独的api调用,我们将问题分解为如下几个部分。

  1. 获取专辑中的所有艺术家
  2. 指出哪些艺术家是乐队
  3. 找到每个乐队的国籍
  4. 将这些值放在一起
Set<String> orgins = album.getMusicians().filter(artist -> artist.getName().startsWith("The")).map(artist -> artist.getNationality()).collect(toSet());

这个例子更清楚的展示了操作链的使用,方法musicians,filter以及map方法都是lazy方法,返回Stream对象,collect是eager方法,返回最终的结果。

获取专辑中的艺术家的时候,与对象返回一个Stream对象,在现存的域对象中,可能不存在返回Stream对象的方法而是返回一个集合或者列表,而你要做的就是调用他们的stream()方法。

可能返回Stream是一个号的选择,返回Stream最大的优势是封装域对象的数据结构。

重构遗留的代码

我们之前讨论了一些重构的信息,然我们看看如何将一段使用循环的遗留的代码转换成为基于Stream的实现,重构中的每一步都会有测试用例,如果你不相信我也应该相信你自己。

这个例子找到长度超过一分钟的歌曲,我们最开始遍历每一个专辑以及专辑中的每一首歌曲,然后查看哪些长度超过一分钟将其加入到一个集合中。

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;
}

这个例子有中两重循环,从表面来看并不能很好理解代码的含义,因此我们决定着手重构,
我们最开始需要重构的就是循环,我们把代码移动到Stream的forEach方法中。

public Set<String> findLongTracks(List<Album> albums) {
    Set<String> trackNames = new HashSet<>();
    albums.stream().forEach(album -> {
        album.getTracks().forEach(track -> {
            if (track.getLength() > 60) {
                String name = track.getName();
                trackNames.add(name);
            }
        });
    });
    return trackNames;
}

第一步中,我们开始使用streams,但是我们没有完全发挥其威力,事实上代码更加丑陋了,我们需要更多的发挥stream的威力,内部的forEach调用就是重构的目标。

这里我们做了三件事:找到指定长度的歌曲,获取名字,将其加入集合中。意味着我们需要调用三个stream来完成工作。找到指定条件的歌曲可以使用filter,获取名字使用map。

public Set<String> findLongTracks(List<Album> albums){
    Set<String> trackNames = new HashSet<>();
    albums.stream().forEach(album ->{
        album.getTracks()
            .filter(track -> track.getLength > 60)
            .map(track -> track.getName())
            .forEach(name -> trackNames.add(name));
    })
    return trackNames;
}

现在,我们加入了更多的stream元素,但是其中仍然有些糟糕的代码,我们不需要嵌套的stream操作,我们需要简洁干净的方法调用。

我们要做的就是将专辑转化成为歌曲流,我们知道,当需要转变以及替换的时候,使用操作map,但是如果将多个stream集合成为一个可以使用flatMap,因此我们把forEach替换为flatMap调用。

public Set<String> findLongTracks(List<Album> albums) {
    Set<String> trackNames = new HashSet<>();
    albums.stream()
        .flatMap(album -> album.getTracks())
        .filter(track -> track.getLength() > 60)
        .map(track -> track.getName())
        .forEach(name -> trackNames.add(name));
    return trackNames;
}

这样代码看起来就好多了,将两重循环调用转变为一个单独的方法序列调用,但是还没有完,在结尾我们仍然手动去创建了一个集合。

虽然我没有展示这种转换,但是我们遇见过其类似的调用,collect(toList())可以构建一个列表, 那么我们也可以使用collect(toSet())构建一个set.

public Set<String> findLongTracks(List<Album> albums) {
            return albumns.stream()
                                .flatMap(album -> album.getTracks())
                                .filter(track -> track.getLength() > 60)
                                .map(track -> track.getName())
                                .collect(toSet());
}
更新于 2016-06-25

查看java更多相关的文章或提一个关于java的问题,也可以与我们一起分享文章