java8 Stream流 理解与应用

  • 1. stream引入
  • 1.1 传统集合的多步遍历代码
  • 1.2 循环遍历的弊端
  • 2. 流式思想概述
  • 3. 如何获取流
  • 3.1. Collection获取流
  • 3.2. Map获取流
  • 3.3. 数组获取流
  • 4. Stram流的常用方法
  • 4.1 挨个处理: forEach
  • 4.2 过滤:filter
  • 4.3 映射:map
  • 4.4 统计个数:count
  • 4.5 取用前几个:limit
  • 4.6 跳过前几个:skip
  • 4.7 组合:concat
  • 4.8 采集: collect
  • 4.8.1 stream流转为字符串并以特定字符分隔
  • 4.8.2 stream流转为list
  • 4.8.3 stream流转为Map
  • 4.9 去重:distinct
  • 4.10 计算唯一值:reduce
  • 4.10.1 示例一:对Integer类型的集合计算求值
  • 4.10.2 示例二:对String类型的集合按规则处理
  • 4.10.2 示例三:对自定义类型的集合按规则处理
  • 结语:



    之前就对java8新曾的stream流有所理解,最近开发项目时碰到了一个业务比较复杂的处理集合的场景,就想到了用stream流来处理。 使用后感慨 stream流是真的6,原来复杂的代码变的很简洁,思路还清晰。 所以来写一篇总结一下用法。


    本篇结构主要是对stream概念的引入与理解,以及常用方法的介绍和使用场景等的说明。

1. stream引入

1.1 传统集合的多步遍历代码

    几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:

@Test
    public void testSream(){
        List<String> list = new ArrayList<>();
        list.add("一红");
        list.add("二白");
        list.add("三黑");
        list.add("三长");
        list.add("四绿");
        list.add("四酒");
        for (String str : list) {
            System.out.println(str);
        }
    }

    这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作

1.2 循环遍历的弊端

    Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How)。现在,仔细体会一下上例代码,可以发现:

  • for循环的语法就是“怎么做”
  • for循环的循环体才是“做什么”

    为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。

    如果希望对集合中的元素进行筛选过滤:

  1. 将集合A根据条件一过滤为子集B;
  2. 然后再根据条件二过滤为子集C。
    那怎么办?在Java 8之前的做法可能为:
@Test
    public void testSream(){
        List<String> list = new ArrayList<>();
        list.add("一红");
        list.add("二白");
        list.add("三黑");
        list.add("三长");
        list.add("四绿");
        list.add("四酒");
        for (String str : list) {
            System.out.println(str);
        }

        //条件一: 去开头为 三 的 , 组成一个新集合
        List<String> listThree = new ArrayList<>();
        for (String str : list) {
            if(str.startsWith("三")){
                listThree.add(str);
            }
        }
       //条件而: 同样的方式将集合再遍历一遍,这里不做冗余示例。。 
    }

    可以看出,每当需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。

    Lambda的衍生物Stream流就是来终结这种繁琐效率低下的方式的。

2. 流式思想概述

    注意这里的流和传统的IO 流不要混淆。

    整体来看,流式思想类似于工厂车间的“生产流水线”。

java stream架构图 java stream教程_stream流


    如上图,每一步都会有一个操作,安装车门、轮胎、喷漆等等的步骤。 这样汽车在流水线上一次下来就差不多安装完成了。

    试想一下,如果安装传统遍历的思想,是不是得安装了车门,就在流水线上走一遍,装个轮胎又一遍,全部下来得多少遍? 这么看起来是不是很蠢? 类比我们传统得java遍历就是这样。。 so 这就是流思想在java编码领域得先进之处。

    在流水线上得操作就像 stream的 filter 、 map 、 skip 都是在对模型(集合等)进行操作,集合元素并没有真正被处理。只有当终结方法 count执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。

    “Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。

  • Stream(流)是一个来自数据源的元素队列。
  1. 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
  2. 数据源 流的来源。 可以是集合,数组 等。
  • 和以前的Collection操作不同, Stream操作还有两个基础的特征:
  1. Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluentstyle)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
  2. 内部迭代: 以前对集合遍历都是通过Iterator或者增强for的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式,流可以直接调用遍历方法。

    当使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。

3. 如何获取流

    java.util.stream.Stream 是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)
    获取一个流非常简单,有以下两种常用的方式

  1. 所有的 Collection 集合都可以通过 stream 默认方法获取流;(个人认为最常用)
  2. Stream 接口的静态方法 of 可以获取数组对应的流。

3.1. Collection获取流

     java.util.Collection 接口中加入了default方法 stream() 用来获取流,所以其所有实现类均可获取流。
示例:

List<String> list = new ArrayList<>();
        list.add("一红");
        list.add("二白");
        list.add("三黑");
        //调用stream()方法获取流对象
        Stream<String> stream = list.stream();

3.2. Map获取流

    java.util.Map 接口不是 Collection 的子接口,且其K-V数据结构不符合流元素的单一特征。
    那么该如何获取呢?

    map 获取对应的流的思路就是将key-value格式转换为单列格式,比如:

  • 通过map的keySet() 方法获取map的key的集合stream流
  • 通过map的values() 方法获取map的value的结合stream流
  • 通过map的entrySet() 方法获取map key-value的集合stream流 -(这种应用场景常见)
    示例:

示例中用到了方法引用 :: ,其中不仅获取了Stream流,获取后顺便有使用了stream的其他的一些方法比如forEach() ,关于方法引用如何使用下篇介绍,stream的其他方法往下看。

Map<String, String> map = new HashMap<>();
        map.put("第一", "吕布");
        map.put("第二", "赵子龙");
        map.put("第三", "马超");
        // 获取key的stream
        Set<String> strings = map.keySet();
        strings.stream().forEach(System.out::println);
        // 获取值的stream
        map.values().stream().forEach(System.out::println);
        // 获取entry的stream
        map.entrySet().stream().forEach(System.out::println);
       // 用entry的流,映射成单独vlues 然后输出 map.entrySet().stream().map(Map.Entry::getValue).forEach(System.out::println);

结果如下:

java stream架构图 java stream教程_java stream架构图_02

3.3. 数组获取流

    如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法of ,使用很简单:

String[] array = {"诸葛亮","郭嘉","郭达"};
        Stream<String> array1 = Stream.of(array);

     注: Stream的静态方法of 其实时一个万能方法,传入of的参数是一个泛型,即collention以及map系列的也可以这样获取 比如:

List<String> lists = new ArrayList<>();
        Stream<List<String>> list1 = Stream.of(lists);

        Stream<Set<Map.Entry<String, String>>> setStream = Stream.of(map.entrySet());

    只不过呢 java.util.Collection 接口中加入了default方法 stream() 用来获取流,所以通常在使用的时候就直接 用集合对象 .stream() 来获取了,比较方便。用哪种都是一样的。

4. Stram流的常用方法

    流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

  • 延迟方法:返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。)
  • 终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持类似 StringBuilder 那样的链式调用。本小节中,终结方法包括 countforEach 方法。

注:stram的更多方法,请自行参考API文档。

4.1 挨个处理: forEach

    虽然方法名字叫 forEach ,但是与for循环中的“for-each”是不同的。可以理解为 作用等同传统方法的遍历。

void forEach(Consumer<? super T> action);

    该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。(好好理解加黑字体处)

java.util.function.Consumer接口是一个消费型接口。
Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。

    使用方法(上边的示例中已有显现):

String[] array = {"诸葛亮","郭嘉","郭达"};
        Stream<String> array1 = Stream.of(array);
        array1.forEach(System.out::println);

结果:

java stream架构图 java stream教程_collect_03

4.2 过滤:filter

    可以通过 filter 方法将一个流转换成另一个子集流。

Stream<T> filter(Predicate<? super T> predicate);

    该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。

java.util.stream.Predicate 函数式接口,其中唯一的抽象方法为:
boolean test(T t);

    该方法将会产生一个boolean值结果,代表指定的条件是否满足。如果结果为true,那么Stream流的 filter 方法将会留用元素;如果结果为false,那么 filter 方法将会舍弃元素。

示例:

String[] array = {"诸葛亮","郭嘉","郭达"};
        Stream<String> array1 = Stream.of(array);
        array1.filter(s->s.startsWith("郭")).forEach(System.out::println);

结果:

java stream架构图 java stream教程_java8_04


    上述示例中通过filter 筛选出了以郭开头的名字,然后直接使用forEarch方法打印了出来。

注: filter方法中使用了Lambda表达式,forEach方法中使用了方法引用。

4.3 映射:map

    如果需要将流中的元素映射到另一个流中,可以使用 map 方法。

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

    该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。

java.util.stream.Function 函数式接口,其中唯一的抽象方法为:
R apply(T t);

    可以将一种T类型转换成为R类型,而这种转换的动作,就称为“映射”。

示例

String[] array = {"诸葛亮","郭嘉","郭达"};
        Stream<String> array1 = Stream.of(array);
        array1.map(str -> str+": 你好厉害!").forEach(System.out::println);

结果

java stream架构图 java stream教程_reduce_05


说明

    示例中将数组中每一个字符串元素后边追加了字符串,然后打印出来。类似的可以将数字集合映射为相应的字符串集合,等等等的使用场景。

    map() 方法的参数 就是映射的规则。

4.4 统计个数:count

    正如旧集合 Collection 当中的 size 方法一样,流提供 count 方法来数一数其中的元素个数:

long count();

    该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。
示例

Stream<String> array1 = Stream.of("诸葛亮","郭嘉","郭达");
        long count = array1.count();
        System.out.println(count);

结果

java stream架构图 java stream教程_stream流_06

4.5 取用前几个:limit

简介:
    limit 方法可以对流进行截取,只取用前n个。

Stream<T> limit(long maxSize);

    参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。
示例:

Stream<String> array1 = Stream.of("诸葛亮","郭嘉","郭达");
       // long count = array1.count();
        array1.limit(2).forEach(System.out::println);

结果:

java stream架构图 java stream教程_java8_07


    可以看到只取了前两个,郭达没有打印出来。

说明:

    在上述示例过程中,可以看到注释掉了一行代码 long count = array1.count();,因为没注释之前在测试过程中出现了异常, 具体原因就是 : 在上述获取到了array1这个stream流之后,使用了count()方法,该方法是“终结类型”的函数。调用了该方法后,会使stream流关闭,后边再调用的时候自然就出现了异常。这一点在开发中也应注意。

4.6 跳过前几个:skip

    如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:

Stream<T> skip(long n);

    如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。
示例

Stream<String> array1 = Stream.of("诸葛亮","郭嘉","郭达");
        array1.skip(2).forEach(System.out::println);

结果

java stream架构图 java stream教程_java8_08


说明

    可以看到,跟上一个示例相反,这次跳过了前两个,结果也如预期只打印了“郭达”。

4.7 组合:concat

    如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :
注意: 是Stream的静态方法

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

示例:

@Test
    public void test2() {
        Stream<String> array1 = Stream.of("诸葛亮","郭嘉","郭达");
        Stream<String> array2 = Stream.of("郭达斯坦森");
        Stream.concat(array1, array2).forEach(System.out::println);
    }

结果

java stream架构图 java stream教程_stream流_09


说明

    可以看到,两个流拼在了一起,然后打印输出如预期。

4.8 采集: collect

    如果希望将一个stream流转换为其他的对象,例如 list、map、或者以特定字符隔开的一个字符串 等等。那么就用 collect方法。

<R, A> R collect(Collector<? super T, A, R> var1);

    该方法的重载许多,这里举几个常用的例子,一般情况就够用了

4.8.1 stream流转为字符串并以特定字符分隔

示例

@Test
    public void test2() {
        Stream<String> array1 = Stream.of("诸葛亮","郭嘉","郭达");
        String collect = array1.collect(Collectors.joining("|"));
        System.out.println(collect);
    }

结果

java stream架构图 java stream教程_collect_10


说明: 将流拼接为一个字符串,每个元素用 “|” 隔开。

4.8.2 stream流转为list

示例

@Test
    public void test2() {
        Stream<String> array1 = Stream.of("诸葛亮","郭嘉","郭达");
        List<String> stringList = array1.collect(Collectors.toList());
        System.out.println(stringList.size());
    }

结果

java stream架构图 java stream教程_reduce_11


说明: 将流转为了list集合,然后打印了该list的长度。

4.8.3 stream流转为Map

    Stream流为单列的,map为多列的,如果需要转的化,通常场景是将单列的元素拆分为key - value

举例

//科目类
    private class Subject{
        int id;
        String name;
// get set 构造方法等省略
@Test
    public void test3() {
        Subject subject1 = new Subject(1,"语文");
        Subject subject2 = new Subject(2,"数学");
        Subject subject3 = new Subject(3,"物理");
        List<Subject> subjects = new ArrayList<>();
        subjects.add(subject1);
        subjects.add(subject2);
        subjects.add(subject3);
        // list-> map
        Map<Integer, String> collect = subjects.stream().collect(Collectors.toMap(Subject::getId, Subject::getName));
        System.out.println(collect.size());
        collect.entrySet().stream().forEach(System.out::println);
    }

结果

java stream架构图 java stream教程_reduce_12


说明

    示例中的转换就是将Subject对象进行拆分,getId-> 作为map中的key, getName-> 作为map的value。

4.9 去重:distinct

    有时候会有对集合或数组去重的去重的需要,用流的话就很方便了。

Stream<T> distinct()

示例

@Test
    public void test2() {
        Stream.of("诸葛亮","郭嘉","郭达","郭嘉").forEach(System.out::println);
        System.out.println("------去重后--------");
        Stream.of("诸葛亮","郭嘉","郭达","郭嘉").distinct().forEach(System.out::println);
    }

结果

java stream架构图 java stream教程_java stream架构图_13


说明: 可以看到去掉了 重复的 “郭嘉”。

4.10 计算唯一值:reduce

    reduce : 减少、降低、缩小
    根据其含义引申:根据一定的规则将Stream中的元素进行计算后返回一个唯一的值。 ,至于什么规则,那就就要根据需求来定义了。

     reduce的往深了说来看 ,使用比较复杂,例如三个参数的等等。 这里只说下常用的,我个人认为,没有必要搞复杂,我的个人追求的就是尽量用简洁的代码来完成复杂的业务(而不是我写的代码别人看不懂就很厉害的样子),当然存在即有其道理,所以我想真正的用到这种复杂参数的是少之又少吧。 故这里就针对2个参数的reduce方法的几种典型用法阐述一下。

    首先说下这个 “唯一值” ,顾名思义不可能是数组,集合等了,只能是一个单一的对象,这个单一对下你个可以是 String、Integer、long 、boolean…,也可以是自定义的一个对象

4.10.1 示例一:对Integer类型的集合计算求值

// 求和
        Integer integer = Stream.of(1, 2, 3, 4, 5).reduce((a, b) -> a+b).get();
        System.out.println(integer);

    这个结果就是 : 15     下边对这个最简单的求和过程阐述一下,其他再复杂的也是这般套路而已。

java stream架构图 java stream教程_reduce_14


    上述示例的求和过程就如上图,根据规律也可以看出来:

  • 第一个参数 , 除了第一次运算时集合的第一个元素值外,其余的均为上次运算的结果。
  • 第二个参数, 从集合第二个元素开始一直迭代到最后一个元素。

    所以Integer时这样,那么不管String或是其他基本类型的还是自定义类型的模式都是这样,万变不离其宗。

    比如比较大小:

Integer integer = Stream.of(1, 2, 3, 4, 5).reduce((a, b) -> a>b ? a:b).get();
        System.out.println(integer);

    结果为: 5

4.10.2 示例二:对String类型的集合按规则处理

示例:

String s = Stream.of("终结者", "二狗", "大浪", "战神", "柳岩").reduce((a, b) -> a + "|" + b).get();
        System.out.println(s);

结果:

java stream架构图 java stream教程_java8_15


    看到这里是不是想起之前的collect方法了,没错用collect方法也可以实现,而且更简单,如下:

String s = Stream.of("终结者", "二狗", "大浪", "战神", "柳岩").collect(Collectors.joining("|"));

    两者达到的效果时完全一样的。

    思考: 使用哪种方法,当然时简单的好了,开发中也没有必要为了使用一个方法而去使用这个方法,使用这个方法的最终目的是为了更好的完成需求。这里的演示只是为了更好了更全面的理解这个reduce方法。 达到目的的方法有千千万万种,如何选择最合适的才是一门很深的学问。

4.10.2 示例三:对自定义类型的集合按规则处理

示例:
    返回各科目中,id值最大的科目对象 ,(Subject科目类,有一个id 和name属性,见上述示例4.8.3)

Subject subject1 = new Subject(1,"语文");
        Subject subject2 = new Subject(2,"数学");
        Subject subject4 = new Subject(4,"化学");
        Subject subject3 = new Subject(3,"物理");
        List<Subject> subjects = new ArrayList<>();
        subjects.add(subject1);
        subjects.add(subject4);
        subjects.add(subject2);
        subjects.add(subject3);
        //返回科目id最大的那一科
        Subject subject = subjects.stream().reduce((a, b) -> a.getId() > b.getId() ? a : b).get();
        System.out.println(subject.toString());

结果:

java stream架构图 java stream教程_java8_16


    道理都是一样的换汤不换药,这里不再赘述。

结语:

总之,Stream 的特性可以归纳为:
(以下引用自 :https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/)
1. 不是数据结构
它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。
它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。
2. 所有 Stream 的操作必须以 lambda 表达式为参数
3. 不支持索引访问
你可以请求第一个元素,但无法请求第二个,第三个,或最后一个。不过请参阅下一项。
4. 很容易生成数组或者 List
5. 惰性化
很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始。
Intermediate 操作永远是惰性化的。
6. 并行能力
当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。
7. 可以是无限的
集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting 操作可以对无限的 Stream 进行运算并很快完成。

    终于写完了这一篇文章,也算是精心的整理了一下,自己对stream有了更深的理解。文档化记录下来,一方面自己日后复看,另一方面也可帮助他人。
    如碰巧有朋友看到了这篇文章,有不足之处还请不吝赐教,提前表示感谢。