Stream流

Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。

Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。

元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

什么是 Stream?

Stream(流)是一个来自数据源的元素队列并支持聚合操作

元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
数据源 流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。
聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。
和以前的Collection操作不同, Stream操作还有两个基础的特征:

Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。

生成流

在 Java 8 中, 集合接口有两个方法来生成流:

stream() − 为集合创建串行流。

parallelStream() − 为集合创建并行流。

List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());

如果你有一个数组,可以使用静态的Stream.of方法

Stream<String> song=Stream.of("gently","down","the","stream");

中间操作

对于数据流来说,中间操作符在执行制定处理程序后,产生的结果仍然是一个数据流。

需要注意的是流操作不改变底层元素

中间操作符包含8种(排除了parallel,sequential,这两个操作并不涉及到对数据流的加工操作):

map(mapToInt,mapToLong,mapToDouble) 转换操作符,把比如A->B,这里默认提供了转int,long,double的操作符。

flatmap(flatmapToInt,flatmapToLong,flatmapToDouble) 拍平操作比如把 int[]{2,3,4} 拍平 变成 2,3,4 也就是从原来的一个数据变成了3个数据,这里默认提供了拍平成int,long,double的操作符。

limit 限流操作,比如数据流中有10个 我只要出前3个就可以使用。

distint 去重操作,对重复元素去重,底层使用了equals方法。

filter 过滤操作,把不想要的数据过滤。

peek 挑出操作,如果想对数据进行某些操作,如:读取、编辑修改等。

skip 跳过操作,跳过某些元素。

sorted(unordered) 排序操作,对元素排序,前提是实现Comparable接口,当然也可以自定义比较器。

例子:

Stream.of("apple","banana","orange").map(e ->e.length()).forEach(e ->System.out.println(e));
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
List<Integer> squaresList = numbers.stream()
                .map(i -> i * i)
                .sorted((x, y) -> {return y - x})
                .collect(Collectors.toList());
  User w = new User("w",10);
        User x = new User("x",11);
        User y = new User("y",12);

        Stream.of(w,x,y)
                .peek(e->{e.setName(e.getAge()+e.getName());}) //重新设置名字 变成 年龄+名字
                .forEach(e->System.out.println(e.toString()));


 User x = new User("x",11);
        User y = new User("y",12);
        User w = new User("w",10);

        Stream.of(w,x,y)
                .sorted((e1,e2)->e1.age>e2.age?1:e1.age==e2.age?0:-1)
                .forEach(e->System.out.println(e.toString()));

终止操作符

数据经过中间加工操作,就轮到终止操作符上场了;终止操作符就是用来对数据进行收集或者消费的,数据到了终止操作这里就不会向下流动了,终止操作符只能使用一次。

collect 收集操作,将所有数据收集起来,这个操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以说Stream 的核心在于Collectors。

count 统计操作,统计最终的数据个数。

findFirst、findAny 查找操作,查找第一个、查找任何一个 返回的类型为Optional。

noneMatch、allMatch、anyMatch 匹配操作,数据流中是否存在符合条件的元素 返回值为bool 值。

min、max 最值操作,需要自定义比较器,返回数据流中最大最小的值。

reduce 规约操作,将整个数据流的值规约为一个值,count、min、max底层就是使用reduce。

forEach、forEachOrdered 遍历操作,这里就是对最终的数据进行消费了。

toArray 数组操作,将数据流的元素转换成数组。

collect

需要注意的是collect方法,官方提供的Collectors有很多API,

数据收集:set、map、list
聚合归约:统计、求和、最值、平均、字符串拼接、规约
前后处理:分区、分组、自定义操作

接下来讲几个比较重要的

1.Collectors.toCollection() 将数据转成Collection,只要是Collection 的实现都可以,例如ArrayList、HashSet ,该方法接受一个Supplier函数式接口(相当于创建对象的工厂,一般为 类名 :: new)。

        //List
        Stream.of(1,2,3,4,5,6,8,9,0)
                .collect(Collectors.toCollection(ArrayList :: new ));
        
        //Set
        Stream.of(1,2,3,4,5,6,8,9,0)
                .collect(Collectors.toCollection(HashSet :: new));

2.Collectors.toList()和Collectors.toSet() 其实和Collectors.toCollection() 差不多,只是指定了容器的类型,默认使用ArrayList 和 HashSet。本来我以为这两个方法的内部会使用到Collectors.toCollection(),结果并不是,而是在内部new了一个CollectorImpl。


         //List
        Stream.of(1,2,3,4,5,6,8,9,0)
                .collect(Collectors.toList());

        //Set
        Stream.of(1,2,3,4,5,6,8,9,0)
                .collect(Collectors.toSet());

3.Collectors.toMap() 和Collectors.toConcurrentMap(),见名知义,收集成Map和ConcurrentMap,默认使用HashMap和ConcurrentHashMap。这里toConcurrentMap()是可以支持并行收集的,这两种类型都有三个重载方法,不管是Map 还是ConcurrentMap,他们和Collection的区别是Map 是K-V 形式的,所以在收集成Map的时候必须指定收集的K(依据)。这里toMap()和toConcurrentMap() 最少参数是,key的获取,要存的value。

Collectors.toMap 有三个重载方法:

//Function表明该参数是一个lamba表达式,作用是进行类型转换
toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper);
toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper,
        BinaryOperator<U> mergeFunction);
toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper,
        BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier);

示例:这里以Student 这个结构为例,Student 包含 id、name。

public class Student{

        //唯一
        private String id;

        private String name;

        public Student() {
        }

        public Student(String id, String name) {
            this.id = id;
            this.name = name;
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }



   Student studentA = new Student("20190001","小明");
        Student studentB = new Student("20190002","小红");
        Student studentC = new Student("20190003","小丁");


        
        //串行收集
     Stream.of(studentA,studentB,studentC)
                .collect(Collectors.toMap(s -> s.getName(),s -> s));

        //并发收集
        Stream.of(studentA,studentB,studentC)
                .parallel()
                .collect(Collectors.toConcurrentMap(s -> s.getName(),s -> s));

那么如果key重复的该怎么处理?这里我们假设有两个id相同Student,如果他们id相同,在转成Map的时候,取name大一个,小的将会被丢弃。


Stream.of(studentA, studentB, studentC)
                .collect(Collectors
                        .toMap(s -> s.getName(),s -> s,
                                (s1, s2) -> {
                            
                                    //这里使用compareTo 方法 s1>s2 会返回1,s1==s2 返回0 ,否则返回-1
                                    if (((Student) s1).name.compareTo(((Student) s2).name) < -1) {
                                        return s2;
                                    } else {
                                        return s1;
                                    }
                                }));

4.Collectors.joining(),拼接,有三个重载方法,底层实现是StringBuilder,通过append方法拼接到一起,并且可以自定义分隔符(这个感觉还是很有用的,很多时候需要把一个list转成一个String,指定分隔符就可以实现了,非常方便)、前缀、后缀。

     
                 Student studentA = new Student("20190001", "小明");
        Student studentB = new Student("20190002", "小红");
        Student studentC = new Student("20190003", "小丁");

             //使用分隔符:201900012019000220190003
        Stream.of(studentA, studentB, studentC)
                .map(Student::getId)
                .collect(Collectors.joining());

        //使用^_^ 作为分隔符
        //20190001^_^20190002^_^20190003
        Stream.of(studentA, studentB, studentC)
                .map(Student::getId)
                .collect(Collectors.joining("^_^"));

        //使用^_^ 作为分隔符
        //[]作为前后缀
        //[20190001^_^20190002^_^20190003]
        Stream.of(studentA, studentB, studentC)
                .map(Student::getId)
                .collect(Collectors.joining("^_^", "[", "]"));

5.Collectors.groupingBy()和Collectors.groupingByConcurrent(),这两者区别也仅是单线程和多线程的使用场景。为什么要groupingBy归类为前后处理呢?groupingBy 是在数据收集前分组的,再将分好组的数据传递给下游的收集器。

这是 groupingBy最长的参数的函数classifier 是分类器,mapFactory map的工厂,downstream下游的收集器,正是downstream 的存在,可以在数据传递个下游之前做很多的骚操作。

public static <T, K, D, A, M extends Map<K, D>>
    Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                                  Supplier<M> mapFactory,
                                  Collector<? super T, A, D> downstream) 

示例:这里将一组数整型数分为正数、负数、零,groupingByConcurrent()的参数也是跟它一样的就不举例了。

             //Map<String,List<Integer>>
        Stream.of(-6, -7, -8, -9, 1, 2, 3, 4, 5, 6)
                .collect(Collectors.groupingBy(integer -> {
                    if (integer < 0) {
                        return "小于";
                    } else if (integer == 0) {
                        return "等于";
                    } else {
                        return "大于";
                    }
                }));

        //Map<String,Set<Integer>>
        //自定义下游收集器
        Stream.of(-6, -7, -8, -9, 1, 2, 3, 4, 5, 6)
                .collect(Collectors.groupingBy(integer -> {
                    if (integer < 0) {
                        return "小于";
                    } else if (integer == 0) {
                        return "等于";
                    } else {
                        return "大于";
                    }
                },Collectors.toSet()));

        //Map<String,Set<Integer>>
        //自定义map容器 和 下游收集器
        Stream.of(-6, -7, -8, -9, 1, 2, 3, 4, 5, 6)
                .collect(Collectors.groupingBy(integer -> {
                    if (integer < 0) {
                        return "小于";
                    } else if (integer == 0) {
                        return "等于";
                    } else {
                        return "大于";
                    }
                },new LinkedHashMap(),Collectors.toSet()));

      //Map<String ,Integer>
      //统计每一组元素的个数
     Stream.of(-6, -7, -8, -9, 1, 2, 3, 4, 5, 6)
                .collect(Collectors.groupingBy(integer -> {
                    if (integer < 0) {
                        return "小于";
                    } else if (integer == 0) {
                        return "等于";
                    } else {
                        return "大于";
                    }
                },Collectors.counting());


reduce

我们先考虑一下min()和max(),其中min()是返回流中的最小值,而max()返回流中最大值,前提是他们存在。他们之间的特点是什么?①都返回了一个值②由一可知,他们是终端操作。如果我们用流API的术语来形容前面这两种特性的结合体的话,它们代表了缩减操作。因为每个缩减操作都把一个流缩减为一个值,好比最大值,最小值。当然流API,把min()和max(),count()这些操作称为特例缩减。即然说到了特例,肯定就有泛化这种概念了,他就是reduce()方法了。

public interface Stream<T> extends BaseStream<T, Stream<T>> {
//、、、忽略其他无关紧要的元素
T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
<U> U reduce(U identity,
          BiFunction<U, ? super T, U> accumulator,
          BinaryOperator<U> combiner);
}

Stream接口定义了三个版本的reduce(),第一个版本返回的是一个T类型的对象,T代表的是流中的元素类型!第二个版本是返回一个Optional类型对象。对于这两种形式,accumulator是一个操作两个值并得到结果的函数。在第一个版本当中,identity是这样一个值,对于涉及identity和流中任意的累积操作,得到的结果就是元素自身,没有任何改变。比如,如果是加法,他就是0,如果是乘法他就是1。

其中的accumulator是一个BinaryOperator的类型,其是一个函数式接口,传入的参数有两个,(a,b)a表示当前结果,b表示下一个操作的元素

第三个版本多用于并行流中,其中combiner是一个合并器,用来将不同流的结果合并,参数同accumulator

例子:

List<Integer> lists = new ArrayList<>();
        lists.add(1);
        lists.add(2);
        lists.add(3);
        lists.add(4);
        lists.add(5);
        lists.add(6);

Integer sum = lists.stream().reduce(0, (a, b) -> a + b);//21
        System.out.println("list的总和为:" + sum);


Integer product = lists.stream().reduce(1, (a, b) -> a * b);
        System.out.println("list的积为:" + product);//720




Integer product2 = lists.stream().reduce(1, (a, b) -> {
            if (b % 2 == 0) return a * b; else return a; //偶数相乘
 });
 System.out.println("list的偶数的积为:" + product2);//48

并行Stream

可以使用集合接口的parallelStream()方法创建并行流 或者 使用流的parallel()方法转化为并行流

需要注意:

如果集合中或者数组中的元素是有序的,那么对应的流也是有序的。但是在使用并行流时,有时候流是无序的就能获得性能上的提升。因为如果流是无序的,那么流的每个部分都可以被单独的操作,而不需要与其他部分协调,从而提升性能。(又是无状态,说好的退休了呢)。所以当流操作的顺序不重要的时候,可以通过BaseStream接口提供的unordered()方法把流转换成一个无序流之后,再进行各种操作。

另外一点:forEach()方法不一定会保留并行流的顺序,如果在对并行流的每个元素执行操作时,也希望保留顺序,那么可以使用forEachOrdered()方法,它的用法和forEach()是一样的。

posted @ 2021-06-21 00:01  刚刚好。  阅读(153)  评论(0)    收藏  举报