zoukankan      html  css  js  c++  java
  • Java8新特性之Stream

    前言

    在想很好了解 Stream 之前,很有必要简单的了解下函数式变成以及Lambda的概念,可以阅读另外一篇

    Java8新特性之Lambda

    大家回忆下日常学习工作中使用的最多的 Java API 是什么?相信很多人的答案和我一样都是集合。我们选择适合的集合数据结构存储数据,而我们之于集合最多的操作就是遍历,实现查询,统计,过滤,合并等业务。

    哪里用Stream

    集合迭代

    外部迭代:通过 for循环,Iterator迭代器遍历集合,手动的拿到集合中每个元素进行相应处理

    • 优点
      • 对于程序的掌控更高
      • 性能强(如果算法功力深厚)
    • 缺点
      • 很多重复的模板代码
      • 需要很多中间临时变量来减少遍历次数
      • 性能完全取决于程序员水平,烧脑
      • 代码不易读
      • 容易出错:例如for循环遍历LinkedList会出错

    内部迭代:只提供对集合中元素的处理逻辑,遍历过程交给库类,Java5提供了foreach,Java8提供了Stream

    • 优点
      • 代码好读
      • 简单,只需要提供处理逻辑
    • 缺点
      • 有些情况性能比外部迭代差一点点
      • 在使用foreach时不能对元素进行赋值操作

    为什么要Stream

    本文要介绍的Stream属于内部迭代,之前我们已经有了foreach减少了我们的代码量,为什么我们还需要Stream呢?

    • 流水线的方式处理集合,结合Lambda爽歪歪
    • 代码超短超好读
    • Stream的开始到结束就相当于一次遍历,允许我们在遍历中拼接多个操作
    • 强调的是对集合的计算逻辑,逻辑可以多次复用
    • 特别繁重的任务可以很轻松的转为并行流,适合类似于大数据处理等业务
    • 成本非常小的实现并行执行效果

    怎么用Stream

    上文我们大概知道了Stream主要服务与集合,流的过程可以总结为三步:

    • 开始操作:读取数据源(如集合)
    • 中间操作:组装中间操作链,形成一条流的流水线
    • 终端操作:一次迭代执行流水线,结束流,并生成结果

    中间操作和终端操作

    第一步流加载集合数据,库类完成我们不需要关心,要想使用好Stream有必要从不同维度了解主要的操作

    • 中间操作:流水线的部件,返回的是this,也就是Stream,此时迭代并没有执行
    • 终端操作:流水线真正开始执行,返回的是处理结果,终端操作过后流关闭
    • 无状态操作:例如对集合中所有元素做转换,或者过滤,不用保存别的元素的处理结果
    • 有状态操作:例如排序,去重操作,需要保存之前集合中元素的状态
    • 非短路操作:按部就班完成迭代,返回处理结果
    • 短路操作:只有一达到预设的条件,立刻停止并返回

    下图给出了Stream给出的一些常用api的基本信息

    基本数据类型流

    为了避免自动装箱拆箱消耗性能,Stream为我们提供了IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,这些特殊的流中提供了range,sum,max等数字类型常用的api

    并行流

    当我们面对计算是否密集的应用开发时,为了充分利用硬件资源,可以简单的通过改变parallel方法将流变成并行执行,但是在使用时有如下注意事项。

    • 并行流是通过 fork/join 线程池来实现的,该池是所有并行流共享的。默认情况,fork/join 池会为每个处理器分配一个线程。假设你有一台16核的机器,这样你就只能创建16个线程。而与此同时其他任务将无法获得线程被阻塞住,所以使用并行流要结合机器和业务场景。
    • 避免在并行流中改变共享状态,小心使用有状态的操作
    • 并行流并不一定就快,要将多个线程的执行结果汇总

    实战

    相信大家最关心的还是实际开发中能帮助我们解决哪些问题,通过一些简单案例熟悉各种操作的用法

    开始操作

    生成空流

    Stream<Object> empty = Stream.empty();
    

    值生成流

    Stream<String> stringStream = Stream.of("1", "2", "3");
    

    数组生成流

    String[] strings = { "1", "2", "3"};
    Stream<String> stream = Arrays.stream(strings);
    

    集合生成流

    List<String> strings1 = Arrays.asList("1", "2", "3");
    Stream<String> stream1 = strings1.stream();
    

    文件生成流

    Stream<String> lines = Files.lines(Paths.get("/c/mnt/"));
    

    函数生成流(无限流)

    // 无限流,流从0开始,下面的每个元素依次加2
    Stream<Integer> iterate = Stream.iterate(0, num -> num + 2);
    // 无限流,流中每个元素都是 0~1 随机数
    Stream<Double> generate = Stream.generate(Math::random);
    

    数值范围生成流

    // 生成0到10的int流
    IntStream intStream = IntStream.rangeClosed(0, 10);
    // 生成0到9的int流
    IntStream intStream1 = IntStream.range(0, 10);
    

    手动生成流

    // 生成有字符串a和数字1的异构的流
    Stream.builder().add("a").add(1).build().forEach(System.out::print);
    

    合并两个流

    Stream.concat(
        Stream.of("1", 22, "333"),
        Stream.of("1", 22, 333)
    ).forEach(System.out::print);
    

    中间操作

    在介绍中间操作时,为了方便学习演示使用到了终端操作中的foreach方法,作用就和我们写的foreach循环类似,遍历执行,不返回值。

    过滤filter

    // 过滤所有空字符串,注意是返回 true的留下
    Stream.of("1", null, "2", "", "3")
        .filter(StringUtils::isNotEmpty)
        .forEach(System.out::print);
    

    去重distinct

    // 去掉重复的2
    Stream.of("1", "2", "2", "2", "3")
        .distinct()
        .forEach(System.out::print);
    

    跳过skip

    // 跳过前两个元素
    Stream.of("1", "2", "3", "4", "5")
        .skip(2)
        .forEach(System.out::print);
    

    截短limit

    // 与skip相反,只留下前2个
    Stream.of("1", "2", "3", "4", "5")
        .limit(2)
        .forEach(System.out::print);
    

    映射map

    @Data
    @AllArgsConstructor
    public class Person {
        private String name;
    }
    
    // 将流中每个字符串转为Person实例
    Stream.of("1", "2", "3")
        .map(Person::new)
        .forEach(System.out::print);
    
    // 将流每个字符串变成其长度,为了避免自动拆箱,使用mapToInt转为IntStream
    Stream.of("1", "2", "3")
        .mapToInt(String::length)
        .forEach(System.out::print);
    

    扁平映射flatMap

    用法和map类似,当我们需要把流中的每个元素全映射另外一个流,也就是数据在流中流里面,这时候操作就不方便,借助flatMap,我们可以把所有第二层流中的元素合并到最外层流

    // 每个字符串按照逗号分隔合并成一个流
    Stream.of("1,2,3", "4,5,6", "7,8,9")
        .flatMap(a -> Stream.of(a.split(",")))
        .forEach(System.out::print);
    

    排序sorted

    Stream.of("4", "3", "5")
        .sorted()
        .forEach(System.out::print);
    
    Stream.of("4", "3", "5")
        .sorted(Comparator.naturalOrder())
        .forEach(System.out::print);
    

    并行流相关parallel,sequential,unordered

    // 串行流转并行流
    Stream.of("1", "2", "3").parallel();
    // 并行流转串行流
    Arrays.asList("1", "2", "3").parallelStream().sequential();
    
    // 在并行流中加上unordered,使得流变成无序,提供并行效率,此时limit相当于随机取2个元素
    Arrays.asList("1", "2", "3", "4", "5")
        .parallelStream()
        .unordered()
        .limit(2).forEach(System.out::print);
    

    调试peek

    用于debug调试代码,在每次执行操作前,看一眼元素

    // 每个元素会打印两遍
    Stream.of("1", "2", "3").peek(System.out::print).forEach(System.out::print);
    

    终端操作

    遍历foreach

    Stream.of("1", "2", "3").forEach(System.out::print);
    
    // 并行流中使用用于保持有序
    Arrays.asList("1", "2", "3").parallelStream().forEachOrdered(System.out::print);
    

    通用统计count,max,min

    // 总个数,返回true|false
    Stream.of("1", "2", "3").count();
    // 最大元素,返回Optional
    Stream.of("1", "2", "3").max(Comparator.naturalOrder());
    // 最小元素,返回Optional
    Stream.of("1", "2", "3").min(Comparator.naturalOrder());
    

    数值流特有统计sum,average,summaryStatistics

    以 IntStream 举例

    // 累加求和
    Stream.of("1", "2", "3").mapToInt(Integer::valueOf).sum();
    // 求平均数
    Stream.of("1", "2", "3").mapToInt(Integer::valueOf).average();
    // 总和,最大值,最小值,平均数,总个数,应有尽有
    Stream.of("1", "2", "3").mapToInt(Integer::valueOf).summaryStatistics();
    

    匹配match

    返回true|false

    // 是否有长度大于 2 的字符串
    Stream.of("1", "22", "333").anyMatch(s -> s.length() > 2);
    // 是否一个长度大于 2 的字符串也没有
    Stream.of("1", "22", "333").noneMatch(s -> s.length() > 2);
    // 是否字符串长度全大于 2
    Stream.of("1", "22", "333").allMatch(s -> s.length() > 2);
    

    查找find

    由于是短路操作,所以只有在串行流中findAny和findFirst才区别明显

    // 找到流中任意一个元素,普通流一般也返回第一个元素,并行流中返回任意元素
    Stream.of("1", "22", "333").findAny();
    // 找到流中第一个元素,普通流和并行流都一样
    Stream.of("1", "22", "333").findFirst();
    

    汇总collect

    把所有处理结果汇总,Collectors收集器里提供了很多常用的汇总操作

    // 将结果汇总成一个list
    Stream.of("1", "22", "333").collect(Collectors.toList());
    

    归约reduce

    谷歌著名的map-reduce理想,用于最后汇总结果

    // 0作为起始的pre,流的结果等于pre乘以自身再加一,直到curr到达最后一个元素
    // (1) pre = 0 curr = 1 计算 pre = 0 * 1 + 1 = 1 
    // (2) pre = 1 curr = 2 计算 pre = 1 * 2 + 1 = 3
    // (3) pre = 3 curr = 3 计算 pre = 3 * 3 + 1 = 10
    // (4) pre = 10 curr = null 返回结果 10
    IntStream.of(1, 2, 3).reduce(0, (pre, curr) -> pre * curr + 1);
    
    // 当然如果不设置初始值,流中第一个元素就是pre
    IntStream.of(1, 2, 3).reduce((pre, curr) -> pre * curr + 1);
    

    转数组toArray

    Stream.of("1", "22", "333").toArray();
    

    获得迭代器iterator

    Stream.of("1", "2", "3").iterator();
    

    获得并行可分迭代器spliterator

    Stream.of("1", "2", "3").spliterator();
    

    流类型判断isParallel

    Stream.of("1", "2", "3").isParallel();
    

    收集器进阶

    在 collect 操作的介绍中,我们用了 Collectors 中提供的 toList 方法将结果汇总成List,collect 是很常用的操作Collectors中有很多有用的方法,这也使得 collect 成为 reduce 更高阶的实现,很多终端方法可用 collect 来实现,如果都不能满足需求我们还可以自己实现一个

    转常用集合

    // 转list
    List<String> collect = Stream.of("1", "2", "3").collect(Collectors.toList());
    
    // 转set
    Set<String> collect = Stream.of("1", "2", "3").collect(Collectors.toSet());
    
    // 转map,key为字符串长度,value为字符串本身
    Map<Integer, String> collect = Stream.of("1", "2", "3")
        .collect(Collectors.toMap(String::length, Function.identity()));
    
    // 转并发版map,key为字符串长度,value为字符串本身
    Map<Integer, String> collect = Stream.of("1", "2", "3")
        .collect(Collectors.toConcurrentMap(String::length, Function.identity()));
    
    // 转指定类型集合
    ArrayList<String> collect = Stream.of("1", "2", "3")
        .collect(Collectors.toCollection(ArrayList::new));
    

    拼接字符串

    // 拼接成一个字符串
    String collect = Stream.of("1", "2", "3").collect(Collectors.joining());
    
    // 拼接成一个字符串,逗号分隔
    String collect = Stream.of("1", "2", "3").collect(Collectors.joining(","));
    

    统计

    都有对应简化版,一些更加灵活多变的操作可以用Collectors

    // 和终端操作中的 max 等价
    Stream.of("1", "2", "3").collect(Collectors.maxBy(Comparator.naturalOrder()));
    
    // 和终端操作中的 min 等价
    Stream.of("1", "2", "3").collect(Collectors.minBy(Comparator.naturalOrder()));
    
    // 和终端操作中的 count 等价
    Stream.of("1", "2", "3").collect(Collectors.counting(Integer::valueOf));
    
    // 和数值流终端操作中的 sum 等价
    Stream.of("1", "2", "3").collect(Collectors.summingInt(Integer::valueOf));
    
    // 和数值流终端操作中的 average 等价
    Stream.of("1", "2", "3").collect(Collectors.averagingInt(Integer::valueOf));
    
    // 和数值流终端操作中的 summaryStatistics 等价
    Stream.of("1", "2", "3").collect(Collectors.summarizingInt(Integer::valueOf));
    

    分组

    可以和其他收集器方法任意组合

    // 以字符串长度分成3组,map的key为长度,value为对应长度字符串list
    Map<Integer, List<String>> collect = Stream.of("1", "22", "33", "4", "555")
        .collect(Collectors.groupingBy(String::length));
    
    // 以字符串长度分成3组,map的key为长度,value为对应的元素个数
    Map<Integer, Long> collect = Stream.of("1", "22", "33", "4", "555")
        .collect(Collectors.groupingBy(String::length, Collectors.counting()));
    
    // 这个例子没有实际意义,展示我们可以进行二级分组,长度分组完毕,再组内以hash值分组
    Map<Integer, Map<Integer, List<String>>> collect = Stream.of("1", "22", "33", "4", "555")
        .collect(Collectors.groupingBy(
            String::length,
            Collectors.groupingBy(String::hashCode)
        ));
    

    分区

    分区时特殊的分组,使用方法类似,特殊是通过谓词表达式只能分成两组,true是一组,false是一组

    // 以长度大于2为标准分区
    Map<Boolean, List<String>> collect = Stream.of("1", "22", "33", "4", "555")
        .collect(Collectors.partitioningBy(s -> s.length() > 2));
    

    归约

    // 和终端操作 reduce 等价
    Stream.of("1", "22", "33", "4", "555")
        .collect(Collectors.reducing(0, Integer::valueOf, Integer::sum))
    

    多操作连接

    // 把流汇总成list,然后再求出其容量
    Integer collect = Stream.of("1", "22", "33", "4", "555")
        .collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
    

    自定义

    有些时候默认的实现有缺陷,或者追求更高的性能我们需要自己实现收集器。只要实现 Collector<T, A , R>接口 中的方法我们就可以获得自己的收集器,其中 T 是元素泛型,A是累加器结果,R是最终返回结果,所有首先我们来看下要实现哪些方法

    • supplier:提供一个容器 A 装结果
    • accumulator:累加器,将元素累加进刚才创建的容器
    • combiner:合并容器的结果
    • finisher:完成操作,将 A 转为 R 返回
    • characteristics:是个定义标识的方法
      • UNORDERED:结果不受流中项目的遍历和累积顺序的影响
      • CONCURRENT:accumulator函数可以从多个线程同时调用
      • IDENTITY_FINISH:表示 finisher 没做任何事情,直接返回了累加的结果,也就是A和R相同
    public static final Collector<String, List<String>, List<String>> myToList = Collector.of(
        // supplier: 创建 A(ArrayList)
        ArrayList::new,
        // accumulator:把每个元素放入 A 中
        (list, el) -> list.add(el),
        // combiner:如果并行拆分成多个流,直接 addAll 合并
        // 如果不想支持并行可以写个空,或抛UnsupportedOperationException异常
        (listA, listB) -> {
            listA.addAll(listB);
            return listA;
        },
        // finisher:不做任何事情,直接返回 A
        Function.identity(),
        // characteristics...:表示 A R 类型相同, 且支持并行流
        Collector.Characteristics.IDENTITY_FINISH,
        Collector.Characteristics.CONCURRENT
    );
    
    // 自定义收集器转成list
    List<String> collect = Stream.of("1", "22", "33", "4", "555").collect(myToList);
    
  • 相关阅读:
    umask设置导致的weblogic中的应用上传的文件没有权限打开
    顺序表查找及其优化(Java)
    前端能力要求
    CSS动画:旋转卡片效果
    CSS居中
    http服务器与https服务器的区别
    phpCURL抓取网页内容
    Node.js概述
    jQuery源码分析
    JavaScript学习书签
  • 原文地址:https://www.cnblogs.com/freshchen/p/12313999.html
Copyright © 2011-2022 走看看