Java 8函数式编程笔记(二)之集合和流(stream)

java Aug 22, 2018

随便说说:

上一篇我们讲了Java 8新增了很多函数接口,这些函数接口是对运算的高度抽象,在这些抽象的基础上,我们可以进一步实现一些具体的API,方便我们的操作。Java 8对核心类库的集合进行了改进,同时引进了新的流(stream)。流式操作可以让程序员在更高的抽象层次上对集合进行操作。

流:

1. 外部迭代和内部迭代

我们平时对集合内部的数据进行处理的时候,通常是进行迭代,然后处理返回的每一个元素。比如下面的一段代码:

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

这样写有几个问题,每次循环的时候都要写一些多余的代码。对for循环改造成并行的方式也比较麻烦。
我们如果探究for循环的原理,for循环其实是一个封装了迭代的语法糖。它首先调用iteration方法,产生一个Iterator对象,控制整个循环,这就是外部迭代
把上面的例子用iterator重写如下:

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

![image_1bjjp5vunro31t5o1n081vm21iim.png-31.7kB][1]

另一种方式是内部迭代。网上没找到内部迭代的定义,我的理解是相对于外部迭代要自己控制迭代的推进,内部迭代的循环交给集合内部进行,程序员把对数据的操作交给集合来执行。
上面的例子如果用新增的流式操作来写的话:
long count = allArtists.stream().filter(artist -> artist.isFrom(" London")).count();

上面每种操作都对应Stream接口的一个方法。filter对Stream对象进行过滤。count方法计算出Stream里面包含多少个对象。

2. 惰性求值和及早求值

什么是惰性求值和及早求值呢?比如说我们出去旅行,有些会前天就把东西打包好了,第二天早上就出门(虽然中午飞机才起飞),这大概就是及早求值了。还有些就是一直拖着,直到最后打包的时间以及出发时间都快要不够了,才开始行动,刚好到飞机场,登机。
在上面的那里例子里面,filter就是一个惰性求值方法,如果你只是调用filter方法。不仅没有结果,而且其实中间也不会运行。只有调用及早求值方法——count才能得到值,整个程序才会运行。
问:“ 为什么要区分惰性求值和及早求值?” 只有在对需要什么样的结果和操作有了更多了解之后,才能更有效率地进行计算。所以中间的惰性求值方法用来描述,你想要得到的stream,最后的及早求值方法来得到你想要的值。

Ps:1. 区分惰性求值和及早求值,看返回值是否是一个stream就可以了
Ps:2. 函数式编程与命令式编程小对比。命令式编程的代码由一系列改变全局状态的语句构成,而函数式编程则将计算的过程抽象成表达式求值。这些表达式由纯数学函数构成,而这些数学函数是第一类对象(我们可以像操作数值一样操作第一类对象)并且没有副作用。由于没有副作用,函数式编程很容易做到线程安全。因此特别适合并发编程。它们可以直接支持并行编程。

常用的流操作:

有了新的API,势必要好好学习一下。函数式编程本身需要一个思维习惯的过程,但是这些操作都能概括成输入N个参数,输出一个值的过程,只是中间的过程不一样而已了。

1. collect(toList())

collect(toList())方法由Stream里的值生成一个列表,是一个及早求值操作。例如: ```List< String> collected = Stream. of(" a", "b", "c").collect( Collectors. toList()); ```

2. map

map方法可以将一种类型的值转换成另外一种类型。 ``` Stream map(Function mapper); ``` 在jdk中源码如上所示,其实map方法接受的是一个Function函数接口,将T类型的数据转换成R类型的数据。

List< String> collected = Stream. of(" a", "b", "hello").map( string -> string.toUpperCase()).collect( toList());
上面的例子中把流中的每一个字符串都转换成了对应的大写形式然后生成了一个list。

用图来表示的话map大概是这样的,把一个正方形变成圆的过程。至于怎么变的❓,由你来编写。

3. filter

遍历数据并检查其中的元素时,可以使用filter方法。

![image_1bjk25mdeg5kfsk1mr41t7211v51t.png-9.5kB][4] filter方法就是从这么多正方形里面选出来你想要的那几个给你 ```Stream filter(Predicate predicate); ``` 它在jdk里面是这样的。我们知道Predicate函数接口是通过传入一个值,然后返回一个boolean值。filter方法最后得到的Stream就是Predicate函数接口中返回为true的值,会被加入到Stream中返回,那如果false的值也需要怎么办,这个当然是可以实现的,后面会介绍。 ```List beginningWithNumbers = Stream.of("a", "1abc", "abc1").filter(value -> isDigit(value.charAt( 0))).collect(toList()); ``` ###4. flatMap  flatMap可以用Stream替换值,然后将多个Stream连接成一个Stream ![image_1bjk3174a1leg8351h1o1rr6fht2a.png-16kB][5] jdk源码如下所示: ```  Stream flatMap(Function> mapper); ``` 和map相比,对照源码可以发现虽然都是用Function接口将一个值转换成另一个值,但是map的Function得到的结果是一个值,flatMap的Function得到的结果是一个Stream。map的操作结果会汇成一个Stream,flatMap也是如此,所以用flatMap可以将多个Stream进行合并。 ``` List together = Stream.of(asList(1, 2), asList(3, 4)).flatMap(numbers -> numbers.stream()).collect(toList()); ```

5. reduce

reduce方法可以实现从一组值中生成一个值。
如下图:

jdk的源码声明如下:
T reduce(T identity, BinaryOperator<T> accumulator);
BinaryOperator是对两个T类型的参数做操作,然后返回一个T类型的接口。也就是reduce方法对整个流里的数据做操作,最后会变成一个数输出。reduce有三种声明,对应三种不同的操作模式。上面的声明等价于下面的代码。

    T result = identity;
    for (T element : this stream)
        result = accumulator.apply(result, element);
    return result; 

下面的例子展示了reduce对list的求和操作。
int count = Stream. of( 1, 2, 3) .reduce( 0, (acc, element) -> acc + element);

6. max和min

就像名字一样max和min是求最大值和最小值的操作。它们是及早求值操作。
举个例子:

List 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();

查找最大和最小元素,首先需要确定以什么指标排序。如上是以查找专辑中的最短曲目为目标,排序的指标就是曲目的长度。min和max需要一个comparator对象。Java 8提供了一个新的静态方法comparing。比起以前通过实现comparator接口,来比较两个对象的某项属性值来说方便了很多。现在只需要提供一个存取方法。max和min方法返回Optional对象。Optional对象是新加入的,它代表一个可能存在也可能不存在的值。通过get方法可以取出Optional对象中的值(如果存在的话)。
Ps:其实count,min和max方法底层都是reduce方法实现的。

最后

集合和流有很多东西需要整理和学习,我也在继续学习中,下次讲怎么把一个以前的函数改成函数式的。新添加的库和stream包下面有很多东西,光这些估计就要看好久了)) ̄▽ ̄")~sigh。

引用:1. [英]沃伯顿(Richard Warburton). Java 8函数式编程 人民邮电出版社.
     2 Java 8 JDK java.util.stream包对应类和接口文档。

zzx

There is my place for writing,coding and reading