浅谈java8中的流的使用

836 阅读8分钟

我们在开发的过程中会大量的使用集合,集合可以将数据进行分组,处理,好多的处理数据的业务逻辑类似于数据库的操作,比如说对一系列的实体根据它其中的某个属性来分组,筛选,像这样的操作,数据库是允许你声明式的指定这些操作的。比如说:

SELECT name FROM apple WHERE weight < 400;

这样的业务逻辑,我们之前的代码实现都是for循环里面,填上一大堆的if判断,新建的临时变量,占用的代码空间很大,而且可读性也不好。

        List<Apple> appleList = new ArrayList<>();
        List<Apple> wantedAppleList = new ArrayList<>();
        for (Apple app : appleList) {
            if (app.getWeight() < 400) {   //筛选
                wantedAppleList.add(app);
            }
        }

        Collections.sort(wantedAppleList, new Comparator<Apple>() {
            public int compare(Apple d1, Apple d2) {  //排序
                return Long.compare(d1.getWeight(), d2.getWeight());
            }
        });

        List<Long> appleIdList = new ArrayList<>();
        for (Apple d : wantedAppleList) {
            appleIdList.add(d.getId());   //获取实体id
        }

看上面的代码,占用的空间很大,而且还会产生和使用垃圾变量,比如代码中的appleIdList,它起到的作用只是一个一次性的中间容器。 在java8之后,这样的语句可以不让它出现了,你不需要担心怎么去显式的实现如何筛选,你只需要说明你想要什么就行了。 如果要处理大量的元素,提高性能,你需要并行处理,利用多核架构,但是写并行代码更复杂,而调试起来也比较难受。比如说,使用synchronized来编写代码,这个代码是迫使代码顺序执行,也就违背了并行执行的初衷,这个在多核cpu上执行所需的成本会更大,多核的cpu的每个处理器内核都有自己独立高速缓存,加锁需要把这些同步缓存同步进行,需要在内核间进行缓慢的缓存一致性协议通信。 痛点说完了,接下来我们说下java8中的流是怎么使用的。

java8中的流

java8中的集合支持一个新的Stream方法,它会返回一个流,到底什么是流呢? 流:从支持数据处理的源生成的元素序列。让我们来咬文嚼字的来分别解释下,

  • 元素序列:和集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值,集合是一种数据结构,它的目的是存储和访问,但是流的目的是表达计算。
  • 源:当然就是提供数据的源头,大部分是集合、数组,由有序列表的产生的流顺序也是一致的。
  • 数据处理操作:流的数据处理功能非常类似于数据库的操作,就是表达出来你要怎么处理。

流也有两个重要的特点:

  • 流水线:Stream 中的很多操作会返回一个流,这样操作就可以链接起来,形成一个大的流水线。
  • 内部迭代:与集合本身的显式迭代不同,Stream流的操作都是在背后进行的。 针对开头的代码,如果是java8的方式我们应该怎么写呢?
//如果是多核架构的话,可以将stream()换成parallelStream()
List<Long> appleIdList = appleList
                .stream()
            //  .parallelStream()   并行处理
                .filter(apple -> apple.getWeight() < 400)
                .sorted(Comparator.comparing(Apple::getWeight))
                .map(Apple::getId)
                .collect(Collectors.toList());

经过对比,思考,我们可以发现,后者的代码是以声明的方式写的,就是陈述了你想要做什么,而不是一大堆的if、for的去实现。这样我们再遇到别的需求的时候,不用再去复制代码了,你只要再按照这样的方式去陈述下你想要的就可以了,你可以把几个简单操作链接起来,来形成一个流水线,就表达复杂的数据处理。

集合和流的内在区别

那么集合和流的内在区别是什么呢?

  1. 比较粗略的说,两者的主要区别就是在于什么时间进行计算。 集合是一个内存中的数据结构,它存储包含着所有的值,每一个元素都是存放在内存里的,元素的计算结果才能成为集合的一部分。 而流呢,是概念上的固定的数据结构,元素都是按需计算的。从另一个角度来说,流就是延迟创建的集合,只要在需要的时候才会计算值,得到结果。套用管理学上的话:需求驱动,实时创造。 举一个例子:用浏览器进行搜索,当你输入一个关键字的时候,Google不会在所有的匹配结果都出来,所有的图片和都下载好之后才返回给你,而是首先给你10个或是20个,当你点击下一页的时候再给你接下来的10个,20个。这也就是只有在需要的时候才会去计算,好像有点类似于懒加载的意思。 有一点流和迭代器比较类似,就是流只能遍历一次,如果你还想在处理一遍,就只能从原始的数据源那里重新生成一个流来遍历(当然了,这里说的集合,不是I/O流)。

  2. 另一个关键的区别就是两者的遍历数据的方式。 Collection接口需要用户去做迭代,for-each,就是外部迭代,去显式的取出每个元素,去处理。而Stream使用的是内部迭代它把迭代已经做了,还把得到的流存储起来。内部迭代的时候,项目可以透明的并行处理,或者是用更好的顺序去处理,Stream库的内部迭代可以自己去选择一种适合你硬件的数据表示和并行实现。

流的操作:我们再来看下上面的代码:filter、sorted、map流水线式的称为中间操作。collect触发流水线操作的是终端操作。

流的使用

流的使用包括三件事:

  • 数据源,集合
  • 中间操作,流水线
  • 终端操作,执行流水线,生成结果

其实流水线的背后理念类似于构建器模式,构建器模式就是用来设置一套配置,也就是这里的中间操作,接着调用built方法,也就是这里的终端操作。关于设计模式,这里就不细说了,以后也会专门的说下各个设计模式,各位小伙伴不要捉急。

筛选 filter:筛选出符合条件的 distinct:去除重复 limit:返回一个不超过给定长度的流,截短 skip:跳过给定长度,如果超过总量,返回空

List<Apple> red = appleList.stream().filter(apple -> apple.getColor().equals("red")).distinct().limit(3).collect(Collectors.toList());

map:对流的元素应用函数,接受一个函数作为参数,并且会把这个函数应用到每一个元素上,并映射到一个新的元素。

List<String> appleNameList = appleList.stream().map(Apple::getName).collect(Collectors.toList());

有的时候也会有这样情况,在使用map操作之后,会产生一个集合或者是数组,而你需要把所有的集合合并为一个集合,这被叫做流的扁平化,接着上代码:

List<String> collect = appleList.stream().map(Apple::getName).map(word -> word.split(" ")).flatMap(Arrays::stream).distinct().collect(Collectors.toList());

flatMap()方法让你把流中的每一个值都换成另一个流,然后把所有的流连接起来,成为一个流。

查找和匹配 anyMatch:流中是否有一个元素符合 allMatch:流中元素是否全部符合 noneMatch:流中无元素符合条件 findAny:查找当前流中的任意元素

Optional<String> any = appleList.stream().map(Apple::getName).map(word -> word.split("")).flatMap(Arrays::stream).distinct().findAny();

Optional是一个容器,代表一个值存在值或不存在,这样就避免出现null了。可以用isPresent()方法判断这个容器是否有值。

归约:

//源码中的reduce
T reduce(T identity, BinaryOperator<T> accumulator);

Integer allSum = numbers.stream().reduce(0, (a, b) -> a + b);

reduce()方法有两个参数,总和变量的初始值,上面的0就是,后面的Lambda是加和的操作。你也可以操作相乘,Lambda反复的结合每个元素,一直到流被规约成一个值。 java8中Integer类现在有了一个静态的sum方法来求和,你还可以这么写:

Integer allInteger = numbers.stream().reduce(0, Integer::sum);

关于reduce,它还有一个重载的变体,看下面,没有初始值,返回一个Optional对象,你们知道为什么会是Optional吗?因为没有初始值,加和操作可能不会得到值。

Optional<Integer> result = numbers.stream().reduce(Integer::sum);
Optional<Integer> maxResult = numbers.stream().reduce(Integer::max);
Optional<Integer> minResult = numbers.stream().reduce(Integer::min);

最后

如果对本文有任何异议或者说有什么好的建议,可以加我好友(公众号后台联系作者),也可以在下面留言区留言。希望这篇文章能帮助大家披荆斩棘,乘风破浪。

这样的分享我会一直持续,你的关注、转发和好看是对我最大的支持,感谢。