Java8新特性:Stream流处理使用总结

一. 概述

  • Stream流是Java8推出的、批量处理数据集合的新特性,在java.util.stream包下。
  • 结合着Java8同期推出的另一项新技术:行为参数化(包括函数式接口、Lambda表达式、方法引用等),Java语言吸收了函数式编程的语法特点,使得程序员能十分便捷地编写处理集合数据的逻辑代码
  • 提高了开发效率的同时,代码可阅读性也大大加强

因此有必要整理下Stream流的常用操作,以备后续处理集合类数据时快速查询和使用

二. 流的概念

  • 概念:从某些源生成的、支持对其中元素批量进行数据处理操作的元素序列
  • 集合可以看做是为相同类型数据存储或访问而设计的一种数据结构或者API类,而流可以看做是对相同类型数据的计算及处理而设计一种API类
  • 当一个流被创建出来后,其内部数据是不可更改的,它不像集合那样有对其中元素增删改的操作

Java8新特性:Stream流处理使用总结_第1张图片

三. 流的使用

流的处理,分为中间操作终端操作两大部分:

  • 中间操作方法的返回结果为流本身,因此可以将多个中间操作串联在一起,形成一个操作链
  • 终端操作的返回结果为其他类型的对象,比如List等。一个流,只可以执行一次终端操作,执行完成后流便不可以被再使用
  • 一个流,实际上只执行一次遍历,此次遍历是在代码执行到终端操作时才执行。在中间操作链中,只是定义了每个操作步骤的具体内容,并没有真正的对其中元素执行遍历方法。

四. API

0. 准备工作:数据源

以下我们创建一个personList的List对象,后续我们会反复利用这个对象生成流,来练习Stream的方法效果。Person类中保存了每个作家的姓名name、年纪age、朝代dynasty、作品集literature

        //为成员变量personList赋值
        personList = List.of(
                new Person("李白", 61, "唐", List.of("将进酒", "行路难")),
                new Person("杜甫", 59, "唐", List.of("登高", "茅屋为秋风所破歌")),
                new Person("苏轼", 64, "宋", List.of("水调歌头·明月几时有")),
                new Person("王勃", 27, "唐", List.of("滕王阁序")),
                new Person("李清照", 71, "宋", List.of("如梦令"))
        );
        

	@Data
	public class Person {
	
	    String name;                //姓名
	    int age;                    //年纪
	    String dynasty;             //朝代
	    List<String> literature;    //作品
	
	    public Person(String name, int age, String dynasty, List<String> literature) {
	        this.name = name;
	        this.age = age;
	        this.dynasty = dynasty;
	        this.literature = literature;
	    }
	}
	

1. 创建流

Java8新特性:Stream流处理使用总结_第2张图片

创建流有四种常见的方式:

  • Stream.of()
  • Stream.iterate()
  • Stream.generate()
  • List.stream()转化

以下为一些创建的示例:

    /**
     * 创建流
     */
    public void create() {

        //方法一:Stream.of() Stream.empty()
        Stream<String> stringStream = Stream.of("a", "b");      //建立一个流,其中有a、b两个元素
        Stream<String> empty = Stream.empty();      //建一个空Stream流,空流的泛型可以指定任何类

        //方法二:用迭代Stream.iterate()方法。   可以创造无限流,这里限制10条:0 2 4 6 8 10 12 14 16 18
        Stream<Integer> numStream = Stream.iterate(0, n -> n + 2).limit(10);
        //打印查看结果
        numStream.forEach(i -> System.out.print(i + " "));
        System.out.println();

        //iterate()方法的重载方法,包括三个入参;三入参很像for循环的写法,中间的谓词判断为false时,迭代中断
        IntStream intStream = IntStream.iterate(0, n -> n < 100, n -> n + 4);
        intStream.forEach(i -> System.out.print(i + " "));
        System.out.println();

        //方法三:用generate来生成
        Stream<Double> generate = Stream.generate(Math::random).limit(5);
        generate.forEach(System.out::println);
        
        /*注意generate尽量在无状态(流中的任意两项没有某种关联)下使用。在有状态时慎用,可能因并发导致前后数据关系出问题;*/
        //以下是一个有状态应用的例子
        IntSupplier fib = new IntSupplier() {
            private int pre = 0;            //保存状态的值,此处是对象的一个成员变量

            @Override
            public int getAsInt() {
                int now = pre + 2;
                pre = now;
                return now;
            }
        };
        //若并发调用,则可能会出现错乱
        List<Integer> parallelList = IntStream.generate(fib).parallel().limit(10000).boxed().collect(toList());
        //打印观察数据,发现日志顺序并没有按照顺序,而是可能出现一片2XXX,紧接着一片1XXXX,又接着一片6XXX,可见多线程下是并行执行,顺序不可保证
        parallelList.stream().sequential().forEach(System.out::println);

        //方法四:通过list等传统集合数据做转化
        Stream<Person> personStream = personList.stream();
    }

2. 中间操作

Java8新特性:Stream流处理使用总结_第3张图片

a. filter()

filter()需传入一个 谓词格式(T-> boolean) 的入参,以此谓词来判断流中每个元素的条件,如返回为true则保留,false则舍弃

    /**
     * 过滤年龄小于60岁的名人
     */
    public void filter() {
        List<Person> filterPersonList = personList.stream()
                .filter(person -> person.getAge() < 60)
                .collect(Collectors.toList());

        //输出:[Person(name=杜甫, age=59, dynasty=唐, literature=[登高, 茅屋为秋风所破歌]), Person(name=王勃, age=27, dynasty=唐, literature=[滕王阁序])]
        System.out.println(filterPersonList);
    }
b. map()

map()方法入参类型为 Function(T-> R) , 流会对其中元素T依次执行此入参方法,转换为类型为R的返回结果。执行map()后,流由原来的Stream 变成了Stream

    /**
     * 将原list映射为另一个list,这里是List -> List
     */
    public void map() {
        List<String> nameList = personList.stream()
                .map(person -> person.getName())
                .collect(Collectors.toList());
        //输出为:[李白, 杜甫, 苏轼, 王勃, 李清照]
        System.out.println(nameList);
    }
  • 转换后的R可以是任意类型。对于返回值为int、float等基本类型数据,则会转换为他们的封装Box类:Integer、Float,成为StreamStream流。
  • 但如果后续又要进行sum()/avg()等运算操作,则流内部又不得不进行静默拆箱,将前一步封装好的Integer再拆箱还原为int基本类型后才能计算。此番反复无谓的拆箱装箱需要浪费不小的计算资源。
  • 因此Java额外提供了针对基本类型数据不需要装箱拆箱的特别流:IntStream/FloatStream等。可以将这种流视为一种特别的流:Stream /Stream
  • 用mapToInt()/mapToFloat()等方法,可转换为此类型
    /**
     * 数据流的映射
     */
    public void map2() {
        IntStream intStream = personList.stream()
//                .map(person -> person.getAge())       //这里的类型是Stream,如果后续是reduce等对数字处理的方法,则会导致隐形拆箱再装箱,耗费资源
                .mapToInt(person -> person.getAge());    //直接是IntStream类型,没有拆箱装箱开销

        //提供一些计算方法,因不需要拆箱装箱所以效率很高
        int sum = intStream.sum();
        //输出为:282
        System.out.println(sum);
    }
c. flatmap()
  • flatmap()也传一个Function,但这个Function的类型为T->Stream,返回是一个Stream流。这样每个对象都会返回一个Stream流,如果换成map()的话,返回结果则为一个嵌套的Stream流:Stream>,是流的流
  • flatMap()可以将每个元素返回的流Stream,归并其中的元素,最终压缩整合成一个流Stream

    /**
     * 将每个人的作品扁平化为一个新的List
     */
    public void flatMap1() {
        List<String> literatureList = personList.stream()
                .flatMap(person -> person.getLiterature().stream())     //将作品的list取出来转换为Stream,然后用flatMap进行扁平化,合并为一个大的Stream
                .collect(Collectors.toList());
        //输出为:[将进酒, 行路难, 登高, 茅屋为秋风所破歌, 水调歌头·明月几时有, 滕王阁序, 如梦令]
        System.out.println(literatureList);
    }

    /**
     * 复杂案例:将每个人的作品跟作者合为一个List, 扁平化为一个新的List
     */
    public void flatMap2() {
        List<List<String>> literatureList = personList.stream()
        		//3. 最后用flatMap进行扁平化,合并为一个大的Stream
                .flatMap(person -> person.getLiterature().stream()			//1:此处返回一个Stream 每个作家作品的流
                        .map(literature -> {								//2:通过map,将上面的Stream转换为Stream>流,每个list有两个对象,为[作家姓名:作品]
                            return Arrays.asList(person.getName(), literature);   //将作者姓名及作品名称组成一个新的数组
                        }))
                .collect(Collectors.toList());
        //输出为:[[李白, 将进酒], [李白, 行路难], [杜甫, 登高], [杜甫, 茅屋为秋风所破歌], [苏轼, 水调歌头·明月几时有], [王勃, 滕王阁序], [李清照, 如梦令]]
        System.out.println(literatureList);

    }
d. distinct()

distinct()会对流元素进行去重

    /**
     * 对列表中的朝代进行去重
     */
    public void distinct() {
        List<String> distinctDynastyList = personList.stream()
                .map(person -> person.getDynasty())
                .distinct()             //去重,如果不去重本例输出为:[唐, 唐, 宋, 唐, 宋]
                .collect(Collectors.toList());

        //输出:[唐, 宋]
        System.out.println(distinctDynastyList);
    }
e. 切片

切片为从流中取部分子集,主要方法有:

  • limit(n) 传入long类型参数,截取原Stream的前n项
  • drop(n) 与limit()正好相反,是舍弃前n项,保留n+1项以后的数据
  • takewhile()传一个谓词,通过谓词判断true或false,连续取满足条件true的项,直到遇到第一个false的停止,并舍弃自此之后的项
  • dropwhile()正好与takewhile()相反,舍弃连续满足条件true的项,直到遇到第一个false的,并从此取之后的数据
    /**
     * 截取前n项
     */
    public void limit() {
        List<Person> subPersonList = personList.stream()
                .limit(2)    //取前两项
                .collect(Collectors.toList());
        //输出:[Person(name=李白, age=61, dynasty=唐, literature=[将进酒, 行路难]), Person(name=杜甫, age=59, dynasty=唐, literature=[登高, 茅屋为秋风所破歌])]
        System.out.println(subPersonList);

    }

    /**
     * 舍弃前n项
     */
    public void skip() {
        List<Person> subPersonList = personList.stream()
                .skip(4)    //取前4项
                .collect(Collectors.toList());
        //输出:[Person(name=李清照, age=71, dynasty=宋, literature=[如梦令])]
        System.out.println(subPersonList);

    }

    /**
     * 切片取前若干项
     */
    public void takewhile() {
        List<Person> subPersonList = personList.stream()
                .takeWhile(person -> person.getAge() > 30)    //取年纪大于30的人,直到遇到第一个年纪小于30的停止
                .collect(Collectors.toList());
        System.out.println(subPersonList);
        //输出:[Person(name=李白, age=61, dynasty=唐, literature=[将进酒, 行路难]), Person(name=杜甫, age=59, dynasty=唐, literature=[登高, 茅屋为秋风所破歌]), Person(name=苏轼, age=64, dynasty=宋, literature=[水调歌头·明月几时有])]
    }

    /**
     * 切片取后若干项
     */
    public void dropWhile() {
        List<Person> subPersonList = personList.stream()
                .dropWhile(person -> person.getAge() > 30)    //舍弃前半部分的年纪大于30的人,直到遇到第一个年纪小于30的,开始取后面的数
                .collect(Collectors.toList());
        //输出:[Person(name=王勃, age=27, dynasty=唐, literature=[滕王阁序]), Person(name=李清照, age=71, dynasty=宋, literature=[如梦令])]
        System.out.println(subPersonList);
    }
f. 查找判断

查找判断包括判断流中元素是否满足某个条件,以及取出某个想要的值,主要用到的有:

  • anyMatch() 检查流中是否匹配有至少一个匹配谓词的执行结果,只要有一个满足,则返回为true
  • noneMatch() 检查流中是否一个都没有匹配谓词的执行结果,只有全部都不满足,才会返回true
  • findAny() 找出满足要求的某个值,不一定是第一个匹配的,速度会比较快
  • findFirst() 找出第一个匹配要求的值
    /**
     * 检查流中是否匹配有至少一个匹配谓词的执行结果
     */
    public void anyMatch() {
        boolean result = personList.stream()
                .anyMatch(person -> person.getAge() == 27);
        //若上式为"== 30", 输出为:false (任意一个都没有匹配)         若为  "== 27", 输出为:true(匹配到了王勃)
        System.out.println(result);
    }

    /**
     * 检查流中是否一个都没有匹配谓词的执行结果
     */
    public void noneMatch() {
        boolean result = personList.stream()
                .noneMatch(person -> person.getAge() == 30);
        //输出为:true
        System.out.println(result);

    }

    /**
     * 找出任意一个匹配的结果(比findFirst执行快,更能并行处理)
     */
    public void findAny() {
        Optional<Person> any = personList.stream()
                .filter(person -> person.getDynasty() == "唐")
                .findAny();

        //输出为:Person(name=李白, age=61, dynasty=唐, literature=[将进酒, 行路难])
        if (any.isPresent()) {
            System.out.println(any.get());
        } else {
            System.out.println("不存在");
        }

    }

    /**
     * 找出第一个匹配的结果
     */
    public void findFirst() {
        Optional<Person> first = personList.stream()
                .filter(person -> person.getDynasty() == "宋")
                .findFirst();

        //输出为:Person(name=苏轼, age=64, dynasty=宋, literature=[水调歌头·明月几时有])
        if (first.isPresent()) {
            System.out.println(first.get());
        } else {
            System.out.println("不存在");
        }
    }

3. 终端操作

Java8新特性:Stream流处理使用总结_第4张图片

a. reduce()

reduce()可以将流中的元素,依次按照方法入参的逻辑进行聚合计算,得到一个结果。具体使用方式参考以下实例:

    /**
     * reduce()聚合
     */
    public void reduce() {
    	//用reduce求和,从初始值逐个进行累加,最终得到所有值的sum数据
        Integer sumAge = personList.stream()
                .map(person -> person.getAge())             //先取年纪,组成新List
                .reduce(0, (a, b) -> a + b);        //首参数为初始值,第二个参数为BiOperator(T,T)->T,本例为(int, int) -> int
        //输出:282
        System.out.println(sumAge);

        Optional<Integer> sumAge2 = personList.stream()
                .map(person -> person.getAge())             //先取年纪,组成新List
                .reduce((a, b) -> a + b);			//无初始值的方案,默认使用流的第一项作为初始值,返回结果可能为null,所以为Opetion类型
        //输出:Optional[282]
        System.out.println(sumAge2);

        //求最大值
        Optional<Integer> sumAge3 = personList.stream()
                .map(person -> person.getAge())             //先取年纪,组成新List
                .reduce(Integer::max);          //可以直接用方法引用,只要保证方法本身是BiOperator类型即可
        //输出:Optional[71]
        System.out.println(sumAge3);
    }
b. collect()

collect()可传入java.util.stream.Collector的实现类,来定义对流中的元素数据做何种聚合处理。java本身也提供了很多常用的Collector实现类或工厂方法,可以拿来主义的直接使用,比如之前见到的toList()方法,就是工厂方法:java.util.stream.Collectors.toList(),返回了一个CollectorImpl对象,实现了将流Stream中的元素转换为List的终端操作。
除此之外还有很多Collector工程类,下面做一些介绍

1)统计相关
    /**
     * 各种统计
     */
    private void statistics() {
        //对年龄进行加总,跟Stream的sum()或reduce()加年纪一个效果,但不会装箱拆箱
        Integer sumAge = personList.stream().collect(summingInt(Person::getAge));	//summingInt()方法可将Stream中的int元素进行累加
        System.out.println(sumAge);
        //平均值
        Double avgAge = personList.stream().collect(averagingInt(Person::getAge));
        //打印:56.4
        System.out.println(avgAge);

        //summarizingInt()可以返回一个多维度统计的结果对象
        IntSummaryStatistics collect = personList.stream().collect(summarizingInt(Person::getAge));
        //打印:IntSummaryStatistics{count=5, sum=282, min=27, average=56.400000, max=71}
        System.out.println(collect);
        
        //maxBy()可以返回按某种条件判断得到的某个T元素,本例为返回年纪最大的人
        Optional<Person> maxPerson = personList.stream().collect(maxBy(Comparator.comparing(Person::getAge)));
        //打印:Optional[Person(name=李清照, age=71, dynasty=宋, literature=[如梦令])]
        System.out.println(maxPerson);
    }
2)joining()聚合拼装字符串
    /**
     * 将流中的每个对象,调用toString()方法串联起来形成一个string字符串
     */
    private void joining() {
        String jsonString = personList.stream().map(Person::getName).collect(Collectors.joining());
        //打印:李白杜甫苏轼王勃李清照
        System.out.println(jsonString);

        //可加间隔参数
        String jsonString2 = personList.stream().map(Person::getName).collect(Collectors.joining(","));
        //打印:李白,杜甫,苏轼,王勃,李清照
        System.out.println(jsonString2);
    }
2)reducing() 聚合方法

聚合reducing(),是更原始的collect方法,很多Stream的聚合方法、Collector接口方法的底层,都是利用reducing方法来实现逻辑

    /**
     * reducing实例
     */
    private void reducing() {
        Integer maxAge = personList.stream().collect(Collectors.reducing(
                0,                          //初始值,类型为T
                Person::getAge,                     //流中对象的取值,返回类型也为T
                Math::max));                        //BiOperator类型的方法入参,为(T, T) -> T
        System.out.println(maxAge);
    }
3)groupingBy() 聚合方法

groupingBy()方法可以接收一个参数,按照此参数的计算,将原Stream分成不同的group组

    private void groupingBy1() {
        Map<String, List<Person>> listMap = personList.stream().collect(Collectors.groupingBy(Person::getDynasty));     //按朝代进行分组
        //{唐=[Person(name=李白, age=61, dynasty=唐, literature=[将进酒, 行路难]),
        // Person(name=杜甫, age=59, dynasty=唐, literature=[登高, 茅屋为秋风所破歌]),
        // Person(name=王勃, age=27, dynasty=唐, literature=[滕王阁序])],
        // 宋=[Person(name=苏轼, age=64, dynasty=宋, literature=[水调歌头·明月几时有]),
        // Person(name=李清照, age=71, dynasty=宋, literature=[如梦令])]}
        System.out.println(listMap);
    }

为了在分组后对子组数据进行更复杂的操作,groupingBy()方法提供了可传入两个参数的重载方法。第一个参数还是用来分组的参数,第二个分组为一个Collector对象,可通过这个Collector对象,对每个组中的子数据集进行后续的计算,整个方法就可以变得很灵活

    /**
     * groupingBy()复杂用法
     */
    private void groupingBy2() {
        //按朝代进行分组,然后挑选出来作品数量大于1的作家
        Map<String, List<Person>> groupFilter = personList.stream().collect(
                Collectors.groupingBy(
                        Person::getDynasty,
                        filtering(person -> person.getLiterature().size() > 1, toList())));     //过滤作品数量大于1
        //{唐=[Person(name=李白, age=61, dynasty=唐, literature=[将进酒, 行路难]), Person(name=杜甫, age=59, dynasty=唐, literature=[登高, 茅屋为秋风所破歌])],
        // 宋=[]}
        /* 可以看到宋朝作为一个分类保留下来了,虽然内容为空   */
        System.out.println(groupFilter);


        //按朝代进行分组,然后将value转换为名字的数据集合
        Map<String, List<String>> group2 = personList.stream().collect(
                Collectors.groupingBy(
                        Person::getDynasty,
                        mapping(Person::getName, toList())));
        //{唐=[李白, 杜甫, 王勃], 宋=[苏轼, 李清照]}
        System.out.println(group2);


        //按朝代进行分组,然后将作品flatmap到一个流中
        Map<String, List<String>> group3 = personList.stream().collect(
                Collectors.groupingBy(
                        Person::getDynasty,
                        flatMapping(item -> item.getLiterature().stream(), toList())));
        //{唐=[将进酒, 行路难, 登高, 茅屋为秋风所破歌, 滕王阁序], 宋=[水调歌头·明月几时有, 如梦令]}
        System.out.println(group3);

        //按朝代分组,再统计各组数量
        Map<String, Long> group4 = personList.stream().collect(
                Collectors.groupingBy(
                        Person::getDynasty,
                        Collectors.counting()));
        //{唐=3, 宋=2}
        System.out.println(group4);

        //按朝代分组,再按年龄条件取各组最大值,注意Value是Optional原对象
        Map<String, Optional<Person>> groupByMaxAge = personList.stream().collect(
                Collectors.groupingBy(
                        Person::getDynasty,
                        maxBy(Comparator.comparing(Person::getAge))
                ));
        //{唐=Optional[Person(name=李白, age=61, dynasty=唐, literature=[将进酒, 行路难])], 宋=Optional[Person(name=李清照, age=71, dynasty=宋, literature=[如梦令])]}
        System.out.println(groupByMaxAge);

        //通过collectingAndThen()方法,对对象做额外的处理
        Map<String, Person> groupByMaxAge2 = personList.stream().collect(
                Collectors.groupingBy(
                        Person::getDynasty,
                        collectingAndThen(          //多加个collectingAndThen(),对Optional做额外的get操作
                                maxBy(Comparator.comparing(Person::getAge)), Optional::get)
                ));
        //{唐=Person(name=李白, age=61, dynasty=唐, literature=[将进酒, 行路难]), 宋=Person(name=李清照, age=71, dynasty=宋, literature=[如梦令])}
        System.out.println(groupByMaxAge2);
    }

因为第二个入参为Collector类对象,而groupingBy()本身也是Collector对象工厂方法,因此可以进行循环嵌套,进行多级分组

    private void multiGroupingBy() {
        //先按朝代、再按作品数量进行分组, 取了作家姓名
        Map<String, Map<Integer, List<String>>> mulGroup = personList.stream().collect(
                Collectors.groupingBy(
                        Person::getDynasty,
                        Collectors.groupingBy(person -> person.getLiterature().size(),
                                mapping(Person::getName, toList()))));
        //{唐={1=[王勃], 2=[李白, 杜甫]}, 宋={1=[苏轼, 李清照]}}
        System.out.println(mulGroup);
    }

总结:
groupingBy()的第二个参数,还可以传一个Collector,子Collector的操作对象为分组之后的每个组下的子Stream,这样就实现了嵌套,可以将前面学过的reducing、mapping、filtering等方法都用在嵌套中

除此之外,还有一种针对boolean类型的特殊分类:partitionBy()

    private void partitionBy() {
        //partitioningBy(), 一种groupingBy的特例,第一个参数为一个谓词,将数组分为false和true两部分
        Map<Boolean, List<String>> partitioningBy = personList.stream().collect(
                partitioningBy(
                        item -> item.getAge() > 60,
                        mapping(Person::getName, toList())
                ));
        //{false=[杜甫, 王勃], true=[李白, 苏轼, 李清照]}
        System.out.println(partitioningBy);
    }

你可能感兴趣的:(Java,java)