Java Stream API 入门篇

Java Stream API 入门篇

Java Stream API 入门篇

代码简洁,函数式编程写出的代码简洁且意图明确,使用stream接口让你从此告别for循环。

多核友好,Java函数式编程使得编写并行程序从未如此简单,你需要的全部就是调用一下parallel()方法。

stream并不是某种数据结构,它只是数据源的一种视图。这里的数据源可以是一个数组,Java容器或I/O channel等。正因如此要得到一个stream通常不会手动创建,而是调用对应的工具方法,比如:

  • 调用Collection.stream()或者Collection.parallelStream()方法;

  • 调用Arrays.stream(T[] array)方法。

常见的stream接口继承关系如图:

为不同数据类型设置不同stream接口,可以1.提高性能,2.增加特定接口函数。

BaseStream

    |--    IntStream   int类型

    |--    LongStream   long类型

    |--    DoubleStream    double类型

    |--    Stream    其他类型

虽然大部分情况下stream是容器调用Collection.stream()方法得到的,但stream和collections有以下不同:

  • 无存储。stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。

  • 为函数式编程而生。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。

  • 惰式执行。stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。

  • 可消费性。stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。

对stream的操作分为为两类,中间操作(intermediate operations)和结束操作(terminal operations),二者特点是:

  1. 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新stream,仅此而已。

  2. 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数。计算完成之后stream就会失效。

操作类型

接口方法

中间操作

无状态

concat() filter() flatMap() map() peek() parallel() sequentila() unordered() mapToInt() mapToLong() mapToDouble() flatMapToInt()

有状态

distinct() sorted() limit() skip()

结束操作

非短路

collect() count() forEach() forEachOrdered() max() min() reduce() toArray()

短路

allMatch() anyMatch() noneMatch() findFirst() fndAny()

区分中间操作和结束操作最简单的方法,就是看方法的返回值,返回值为stream的大都是中间操作,否则是结束操作。

stream方法使用

stream跟函数接口关系非常紧密,没有函数接口stream就无法工作。回顾一下:函数接口是指内部只有一个抽象方法的接口。通常函数接口出现的地方都可以使用Lambda表达式,所以不必记忆函数接口的名字。

max()--最大元素

Optional<String> max = stream.max((s1,s2)->s1.length()-s2.length());
            String string = max.get();

findAny()--任一一元素

    Optional<String> optional = stream.filter(s->s.length()==1).findAny();
            String string = optional.get();

anyMatch()--任一匹配

boolean allMatch = stream.anyMatch(s->s.length()==3);

noneMatch()--无一匹配

stream.noneMatch(s->s.length()==3);

allMatch()--全部匹配

stream.allMatch(s->s.length()==1);

toArray()--转成数组

Object[] array = stream.toArray();

forEachOrdered()--遍历

并行流中仍然按照原顺序遍历

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);  
    list.stream().parallel().forEach(x -> System.out.print(x));  
    list.stream().parallel().forEachOrdered(x -> System.out.print(x));

skip()--去除前n个元素

Stream<String> stream = Stream.of("1", "2", "3", "4");
        stream.skip(2)//截去前两个元素,只取后面的元素
        .forEach(System.out::println);

limit()--指定元素大小

    Stream<String> stream = Stream.of("1", "2", "3", "4");
        stream.limit(2l)//限制两个元素,只取前两个元素
        .forEach(System.out::println);

mapToInt()--元素转int

Stream<String> stream = Stream.of("1", "2", "3", "4");
        stream.mapToInt(Integer::parseInt).forEach(System.out::println);

parallel()--并行流

流可以是顺序的也可以是并行的。顺序流的操作是在单线程上执行的,而并行流的操作是在多线程上并发执行的。

stream.parallel().forEach(System.out::println);

peek()--遍历

对stream中的每一个元素进行遍历消费,返回一个新流 Stream peek(Consumer<? super T> action);

    Stream<String> stream = Stream.of("I", "love", "you", "too");
        stream.peek(TestM::getStr)//中间操作
          .forEach(System.out::println);//结束操作

count()--元素个数

Stream<String> stream = Stream.of("I", "love", "you", "too");
        long count = stream.count();//4个元素

forEach()--遍历

我们对forEach()方法并不陌生,在Collection中我们已经见过。方法签名为void forEach(Consumer<? super E> action),作用是对容器中的每个元素执行action指定的动作,也就是对元素进行遍历。

        // 使用Stream.forEach()迭代
        String[] arry = {"I", "love", "you", "too"}; 
        Stream<String> stream = Stream.of(arry);
        stream.forEach(str -> System.out.println(str));

filter()--过滤

函数原型为Stream filter(Predicate<? super T> predicate),作用是返回一个只包含满足predicate条件元素的Stream。

        String[] arry = {"I", "love", "you", "too"}; 
        Stream<String> stream = Stream.of(arry);
        stream.filter(str->str.length()==3)//中间操作,过滤元素长度等于3的元素
              .forEach(str -> System.out.println(str));//最终操作

distinct()--去重

函数原型为Stream distinct(),作用是返回一个去除重复元素之后的Stream

        String[] arry = {"I", "love", "you", "too","too","you"}; 
        Stream<String> stream = Stream.of(arry);
        stream.distinct()//中间操作,去除重复元素
              .forEach(str -> System.out.println(str));//最终操作

sorted()--排序

排序函数有两个,一个是用自然顺序排序,一个是使用自定义比较器排序,函数原型分别为Stream sorted()和Stream sorted(Comparator<? super T> comparator)。

        Stream<String> stream= Stream.of("I", "love", "you", "too");
        stream.sorted((str1, str2) -> str1.length()-str2.length())//中间操作,按元素长度排序
              .forEach(str -> System.out.println(str));//最终操作,遍历打印

map()--转换

函数原型为 Stream map(Function<? super T,? extends R> mapper),作用是返回一个对当前所有元素执行执行mapper之后的结果组成的Stream。直观的说,就是对每个元素按照某种操作进行转换,转换前后Stream中元素的个数不会改变,但元素的类型取决于转换之后的类型。

        Stream<String> stream = Stream.of("I", "love", "you", "too");
        stream.map(str -> str.toUpperCase())//转换成大写
              .forEach(str -> System.out.println(str));

flatMap()--合并

函数原型为 Stream flatMap(Function<? super T,? extends Stream<? extends R>> mapper),作用是对每个元素执行mapper指定的操作,并用所有mapper返回的Stream中的元素组成一个新的Stream作为最终返回结果。说起来太拗口,通俗的讲flatMap()的作用就相当于把原stream中的所有元素都”摊平”之后组成的Stream,转换前后元素的个数和类型都可能会改变。

        Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
        stream.flatMap(list -> list.stream())//转换到一个流
              .forEach(i -> System.out.println(i));

Java Stream API 进阶篇

规约操作(reduction operation)又被称作折叠操作(fold),是通过某个连接动作将所有元素汇总成一个汇总结果的过程。元素求和、求最大值或最小值、求出元素总个数、将所有元素转换成一个列表或集合,都属于规约操作。Stream类库有两个通用的规约操作reduce()和collect(),也有一些为简化书写而设计的专用规约操作,比如sum()、max()、min()、count()等。

最大或最小值这类规约操作很好理解(至少方法语义上是这样),我们着重介绍reduce()和collect()

reduce()--生成一个值

reduce操作可以实现从一组元素中生成一个值,sum()、max()、min()、count()等都是reduce操作,将他们单独设为函数只是因为常用。

reduce()的方法定义有三种重写形式:

  • Optional reduce(BinaryOperator accumulator)

  • T reduce(T identity, BinaryOperator accumulator)

  • U reduce(U identity, BiFunction accumulator, BinaryOperator combiner)

Optional<T> reduce(BinaryOperator<T> accumulator)

我们先看第一个变形,其接受一个函数接口BinaryOperator<T>,而这个接口又继承于BiFunction<T, T, T>
BinaryOperator接口,可以看到reduce方法接受一个函数,这个函数有两个参数,第一个参数是上次函数执行的返回值(也称为中间结果),第二个参数是stream中的元素,这个函数把这两个值相加,得到的和会被赋值给下次执行这个函数的第一个参数。要注意的是:第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素
第一变形:未定义初始值,从而第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素

T reduce(T identity, BinaryOperator<T> accumulator)

第二个变形,与第一种变形相同的是都会接受一个BinaryOperator函数接口,不同的是其会接受一个identity参数,用来指定Stream循环的初始值。定义了初始值,从而第一次执行的时候第一个参数的值是初始值,第二个参数是Stream的第一个元素。如果Stream为空,就直接返回该值。另一方面,该方法不会返回Optional,因为该方法不会出现null。


<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
Stream是支持并发操作的,为了避免竞争,对于reduce线程都会有独立的result,combiner的作用在于合并每个线程的result得到最终结果。这也说明了了第三个函数参数的数据类型必须为返回数据类型了。

虽然函数定义越来越长,但语义不曾改变,多的参数只是为了指明初始值(参数identity),或者是指定并行执行时多个部分结果的合并方式(参数combiner)。reduce()最常用的场景就是从一堆值中生成一个值。用这么复杂的函数去求一个最大或最小值,你是不是觉得设计者有病。其实不然,因为“大”和“小”或者“求和”有时会有不同的语义。

Stream<String> stream = Stream.of("I", "love", "you", "too");
Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
//Optional<String> longest = stream.max((s1, s2) -> s1.length()-s2.length());
System.out.println(longest.get());

上述代码会选出最长的单词love,其中Optional是(一个)值的容器,使用它可以避免null值的麻烦。当然可以使用Stream.max(Comparator<? super T> comparator)方法来达到同等效果,但reduce()自有其存在的理由。

需求:求出一组单词的长度之和。这是个“求和”操作,操作对象输入类型是String,而结果类型是Integer。

// 求单词长度之和
Stream<String> stream = Stream.of("I", "love", "you", "too");
Integer lengthSum = stream.reduce(0, // 初始值 // (1)
        (sum, str) -> sum+str.length(), // 累加器 // (2)
        (a, b) -> a+b); // 部分和拼接器,并行执行时才会用到 // (3)
// int lengthSum = stream.mapToInt(str -> str.length()).sum();
System.out.println(lengthSum);

上述代码标号(2)处将i. 字符串映射成长度,ii. 并和当前累加和相加。这显然是两步操作,使用reduce()函数将这两步合二为一,更有助于提升性能。如果想要使用map()和sum()组合来达到上述目的,也是可以的。

reduce()擅长的是生成一个值,如果想要从Stream生成一个集合或者Map等复杂的对象该怎么办呢?终极武器collect()横空出世!

collect()--生成一个集合

Stream -> collection

不夸张的讲,如果你发现某个功能在Stream接口中没找到,十有八九可以通过collect()方法实现。collect()是Stream接口方法中最灵活的一个,学会它才算真正入门Java函数式编程。先看几个热身的小例子:

        Stream<String> stream = Stream.of("I", "love", "you", "too");
        List<String> list = stream.collect(Collectors.toList()); // stream -> list
         Set<String> set = stream.collect(Collectors.toSet()); // Stream -> set
         Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); // Stream -> map
        ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// Stream -> arrayList
        HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// Stream -> HashSet

上述代码分别列举了如何将Stream转换成List、Set和Map。虽然代码语义很明确,可是我们仍然会有几个疑问:

  1. Function.identity()是干什么的?

  2. String::length是什么意思?

  3. Collectors是个什么东西?收集器(Collector)是为Stream.collect()方法量身打造的工具接口(类)

接口的静态方法和默认方法

Function是一个接口,那么Function.identity()是什么意思呢?这要从两方面解释:

  1. Java 8允许在接口中加入具体方法。接口中的具体方法有两种,default方法和static方法,identity()就是Function接口的一个静态方法。

  2. Function.identity()返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。

方法引用

诸如String::length的语法形式叫做方法引用(method references),这种语法用来替代某些特定形式Lambda表达式。如果Lambda表达式的全部内容就是调用一个已有的方法,那么可以用方法引用来替代Lambda表达式。方法引用可以细分为四类:

方法引用的类别

举例

引用静态方法

Integer::sum

应用对象方法

list::add

应用某个类的方法

String::length

应用构造方法

HashMap::new

Stream -> map

前面已经说过Stream背后依赖于某种数据源,数据源可以是数组、容器等,但不能是Map。反过来从Stream生成Map是可以的,但我们要想清楚Map的key和value分别代表什么,根本原因是我们要想清楚要干什么。通常在三种情况下collect()的结果会是Map:

  1. 使用Collectors.toMap()生成的收集器,用户需要指定如何生成Map的key和value。

  2. 使用Collectors.partitioningBy()生成的收集器,对元素进行二分区操作时用到。

  3. 使用Collectors.groupingBy()生成的收集器,对元素做group操作时用到。

情况1:使用toMap()生成的收集器,这种情况是最直接的,前面例子中已提到,这是和Collectors.toCollection()并列的方法。如下代码展示将学生列表转换成由<学生,GPA>组成的Map。非常直观,无需多言。

// 使用toMap()统计学生GPA
Map<Student, Double> studentToGPA =students.stream().collect(
                      Collectors.toMap(
                          Functions.identity(),// 如何生成key
                          student -> computeGPA(student)// 如何生成value
                    )
);

情况2:使用partitioningBy()生成的收集器,这种情况适用于将Stream中的元素依据某个二值逻辑(满足条件,或不满足)分成互补相交的两部分,比如男女性别、成绩及格与否等。下列代码展示将学生分成成绩及格或不及格的两部分。

// Partition students into passing and failing
Map<Boolean, List<Student>> passingFailing = students.stream()
         .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));

情况3:使用groupingBy()生成的收集器,这是比较灵活的一种情况。跟SQL中的group by语句类似,这里的groupingBy()也是按照某个属性对数据进行分组,属性相同的元素会被对应到Map的同一个key上。下列代码展示将员工按照部门进行分组:

// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::getDepartment));

以上只是分组的最基本用法,有些时候仅仅分组是不够的。在SQL中使用group by是为了协助其他查询,比如1. 先将员工按照部门分组,2. 然后统计每个部门员工的人数。Java类库设计者也考虑到了这种情况,增强版的groupingBy()能够满足这种需求。增强版的groupingBy()允许我们对元素分组之后再执行某种运算,比如求和、计数、平均值、类型转换等。这种先将元素分组的收集器叫做上游收集器,之后执行其他运算的收集器叫做下游收集器(downstream Collector)。

// 使用下游收集器统计每个部门的人数
Map<Department, Integer> totalByDept = employees.stream()
                    .collect(Collectors.groupingBy(Employee::getDepartment,
                                                   Collectors.counting()));// 下游收集器

上面代码的逻辑是不是越看越像SQL?高度非结构化。还有更狠的,下游收集器还可以包含更下游的收集器,这绝不是为了炫技而增加的把戏,而是实际场景需要。考虑将员工按照部门分组的场景,如果我们想得到每个员工的名字(字符串),而不是一个个Employee对象,可通过如下方式做到:

// 按照部门对员工分布组,并只保留员工的名字
Map<Department, List<String>> byDept = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment,
                        Collectors.mapping(Employee::getName,// 下游收集器
                                Collectors.toList())));// 更下游的收集器

拼接字符串

// 使用Collectors.joining()拼接字符串
         Stream<String> stream = Stream.of("I", "love", "you");
         //String joined = stream.collect(Collectors.joining());// "Iloveyou"
         //String joined = stream.collect(Collectors.joining(","));// "I,love,you"
         String joined = stream.collect(Collectors.joining(",", "{", "}"));// "{I,love,you}"

Last updated