Java8 Stream 入门看这篇就够了

初识 Stream

Java 8 API添加了一个新的抽象称为流 Stream,可以让你以一种声明的方式处理数据。使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

图上的流程转换为 Java 代码为:

List transactionsIds = 
widgets.stream()  // Source
             .filter(b -> b.getColor() == RED)  // Operations-filter
             .sorted((x,y) -> x.getWeight() - y.getWeight())   // Operations-sorted
             .mapToInt(Widget::getWeight)   // Operations-map
             .sum();  // Results-sum

什么是 Stream?

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

  • 无存储:不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
  • 为函数式编程而生:对 stream 的修改都不会修改其数据源,比如对 stream 执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新 stream。 (peek 对可变对象可以修改)
  • 惰式执行:stream 上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
  • 可消费性:stream 只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。

Stream操作还有两个基础的特征:

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

关于 Stream 的相关API实现原理, 感兴趣的可以去看下: 浅析 Java8 Stream 原理

创建流

stream 常见的创建的方式主要有Arrays、Collection、Stream 静态方法等,这里代码列举其中最常见的几种:

package com.company.designModel;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;

public class Java8StreamTest {

    public static void main(String[] args) {
        // 1.数组创建流
        Integer[] arrays = {9, 5, 2, 7};
        Stream arraysStream = Arrays.stream(arrays);

        // 2.Collection 创建流
        ArrayList list = new ArrayList<>();
        list.add("Amy");
        list.add("Angst");
        Stream stream = list.stream();
        Stream parallelStream = list.parallelStream();  // 这里 parallelStream 创建的是一个并行流

        // 3. Stream.of
        Stream.of(1, 24, 8, 6, 10, 4, 8, 3, 2, 8, 6).skip(2).limit(5).forEach(System.out::println);
        // empty() 可以创建一个空的流
        Stream emptyStream = Stream.empty();

        // 4.Stream.generate()
        Stream.generate(Math::random).limit(10).skip(1).forEach(System.out::print);

        // 5.Stream.iterate()
        Stream.iterate(0, item -> ++ item).limit(10).forEach(System.out::print);

        // 6.Stream.builder()
        Stream buildStream = Stream.builder().add("Amy").add("Angst").build();

        // 7. Stream.concat() 合并创建一个新的流
        Stream concatStream = Stream.concat(stream, parallelStream);
    }
}


这里有几个需要关注的点:

  • 查看 Stream 源码的话,你会发现 of() 方法内部其实调用了 Arrays.stream() 方法。源码注释有提到 “从数组创建的流是安全的”
    /**
     * Returns a sequential ordered stream whose elements are the specified values.
     *
     * @param  the type of stream elements
     * @param values the elements of the new stream
     * @return the new stream
     */
    @SafeVarargs
    @SuppressWarnings("varargs")   // Creating a stream from an array is safe
    public static Stream of(T... values) {
        return Arrays.stream(values);
    }
  • Stream.generate(): 生成一个无限长度的Stream,其元素的生成是通过给定的Supplier(这个接口可以看成一个对象的工厂,每次调用返回一个给定类型的对象), 其中值是随机的。这个无限长度 Stream 是懒加载,一般都会配合Stream的limit()方法来做分页使用。
  • Stream.iterate(): 也是生成无限长度的Stream,和 generator 不同的是,其元素的生成是重复对给定的种子值(seed)调用用户指定函数来生成的. 其中包含的元素可以认为是:seed,f(seed), f(f(seed))无限循环。

操作流

流的操作类型分为两种:

  • 中间操作(Intermediate):一个流可以后面跟随零个或多个intermediate操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用(链式操作)。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。

    • 有状态(Stateless):指该操作只有拿到所有元素之后才能继续下去
    • 无状态(Stateful):指元素的处理不受之前元素的影响
  • 终止操作(Terminal):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以,这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

    • 非短路操作: 指必须处理所有元素才能得到最终结果
    • 短路操作: 指遇到某些符合条件的元素就可以得到最终结果,如 A || B,只要A为true,则无需判断B的结果

代码演示

Intermediate方法
  • filter(): 用于筛选数据,返回由匹配的元素组成的流。
  • limit(): 切片限流可以当做分页来使用,返回被截断的流。
  • skip():要跳过前面元素n的数量,返回由该流的剩余元素组成的流。
  • distinct():去重操作,返回由不同元素组成的流。
public static void main(String[] args) {
    ArrayList list = new ArrayList<>();
    list.add("Vivian");
    list.add("Amy");
    list.add("Amy");
    list.add("Angst");
    list.add("Zx");

    list.stream().  // 创建一个list流
            distinct().  // 去重操作
            skip(1).  // 跳过第一个元素
            filter(o -> o.startsWith("A")).  // 过滤以A开头的元素
            limit(2).  // 切片选择保留几个
            forEach(System.out::println);  // 输出结果

}

结果输出:

Amy
Angst

Process finished with exit code 0
  • map():接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
  • flatMap():接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
public static void main(String[] args) {
    ArrayList list = new ArrayList<>();
    list.add("Vivian");
    list.add("Amy");
    list.add("Angst");

    // 1.map
    list.stream().map(String::toLowerCase).forEach(System.out::println);
    list.stream().mapToInt(String::length).forEach(System.out::println);
    list.stream().mapToDouble(String::length).forEach(System.out::println);
    list.stream().mapToLong(String::length).forEach(System.out::println);

    // 2. flatMap 同 map 作用类似,区别就是将每个元素重新组成Stream,并将这些Stream 串行合并成一条Stream.
    list.stream().flatMap(o-> Stream.of(o.toLowerCase())).forEach(System.out::println);
    list.stream().flatMapToInt(o-> IntStream.of(o.length())).forEach(System.out::println);
    list.stream().flatMapToDouble(o-> DoubleStream.of(o.length())).forEach(System.out::println);
    list.stream().flatMapToLong(o-> LongStream.of(o.length())).forEach(System.out::println);

}

结果输出:

vivian
amy
angst
6
3
5
6.0
3.0
5.0
6
3
5

vivian
amy
angst
6
3
5
6.0
3.0
5.0
6
3
5

Process finished with exit code 0
  • sorted():自然排序,流中元素需实现Comparable接口, 也可以自定义 Comparator
public static void main(String[] args) {
    ArrayList list = new ArrayList<>();
    list.add("Vivian");
    list.add("Amy");
    list.add("Angst");

    list.stream().sorted().forEach(System.out::println);  // 默认排序
    list.stream().sorted((x, y) -> y.length() - x.length()).forEach(System.out::println);  // 自定义排序
    
}

结果输出:
Amy
Angst
Vivian

Vivian
Angst
Amy

Process finished with exit code 0
  • peek():如同于map(),能得到流中的每一个元素。但map接收的是一个 Function 表达式,有返回值。而peek接收的是Consumer,没有返回值。
public class Java8StreamTestDemo {

    public static void main(String[] args) {
        // 1.不可变对象
        ArrayList arrayList = new ArrayList<>();
        arrayList.add("Vivian");
        arrayList.add("Amy");
        arrayList.add("Angst");

        // 修改元素首字母小写,实际结果并没有修改成功
        arrayList.stream().peek(String::toLowerCase).forEach(o -> System.out.println("toLowerCase:" + o));  


        // 2.可变对象
        ArrayList list = new ArrayList<>();
        list.add(new Food("果汁"));
        list.add(new Food("奶茶"));
        list.add(new Food("牛奶"));

        // peek()方法存在的主要目的是用调试,通过 peek() 方法可以看到流中的数据经过每个处理点时的状态。
        list.stream().peek(o -> System.out.println("debugger: "+ o.getCoca())).count();

        // 除去用于调试,peek()在需要修改元素内部状态的场景也非常有用,比如修改 Coca 的值 ,当然也可以使用map()和flatMap实现.
        list.stream().peek(o->o.setCoca("饭前:"+ o.getCoca())).forEach(System.out::println);


    }
    
    // 定义一个可变对象
    static class Food {
        private String coca="饮料";
        public Food(String coca) { this.coca = coca; }
        public String getCoca() { return coca; }
        public void setCoca(String coca) { this.coca = coca; }

        @Override
        public String toString() {
            return "Food{" +
                    "coca='" + coca + '\'' +
                    '}';
        }
    }

}
结果输出:

toLowerCase:Vivian
toLowerCase:Amy
toLowerCase:Angst
debugger: 果汁
debugger: 奶茶
debugger: 牛奶
Food{coca='饭前:果汁'}
Food{coca='饭前:奶茶'}
Food{coca='饭前:牛奶'}

Process finished with exit code 0

注意:map函数对Stream中元素执行的是映射操作,会以新的元素(map的结果)填充新的Stream。严格的讲map不是修改原来的元素。peek只能消费Stream中的元素,是否可以更该Stream中的元素,取决于Stream中的元素是否是不可变对象。如果是不可变对象,则不可修改Stream中的元素;如果是可变对象,则可以修改对象的值,但是无法修改对象的引用。


Terminal方法
  • max(): 返回此流的最大元素
  • min(): 返回此流的最小元素
  • count(): 返回此流中的元素计数
  • findFirst(): 返回此流中第一个元素
  • findAny(): 取任意一个元素,正常情况下一般会取第一个元素,在并行流的情况下会随机取一个元素
public static void main(String[] args) {
    ArrayList arrayList = new ArrayList<>();
    arrayList.add("Vivian");
    arrayList.add("Amy");
    arrayList.add("Angst");

    System.out.println(arrayList.stream().map(String::length).max(Integer::compareTo));
    System.out.println(arrayList.stream().map(String::length).min(Integer::compareTo));
    System.out.println(arrayList.stream().map(String::length).count());

    System.out.println(arrayList.stream().map(String::length).findFirst());
    // 在 parallel 流中存在每次输出的结果不一致
    System.out.println(arrayList.stream().parallel().map(String::length).findAny());
}

结果输出:

Optional[6]
Optional[3]
3

Optional[6]
Optional[3]

Process finished with exit code 0
  • andMatch(): 返回流中的元素是否与所提供的匹配
  • allMatch(): 返回流的所有元素是否与指定的条件匹配。
  • noneMatch(): 返回流中是否没有匹配谓词的元素
public static void main(String[] args) {
    ArrayList arrayList = new ArrayList<>();
    arrayList.add("Vivian");
    arrayList.add("Amy");
    arrayList.add("Angst");

    // 匹配成功返回 true, 否则 false
    System.out.println(arrayList.stream().noneMatch(o->o.equals("amy")));  // 空匹配
    System.out.println(arrayList.stream().anyMatch(o->o.equals("Amy")));  // 任意匹配
    System.out.println(arrayList.stream().allMatch(o->o.equals("Amy")));  // 全匹配
}

结果输出:

true
true
false

Process finished with exit code 0
  • reduce(): 用于对 stream 中元素进行聚合求值
// 规约操作
System.out.println(Stream.of(1, 9, 5, 2, 7, -1).reduce(0, Integer::sum));


23

Process finished with exit code 0
  • forEach(): 遍历元素
  • iterator(): 返回一个迭代器
public static void main(String[] args) {
    ArrayList arrayList = new ArrayList<>();
    arrayList.add("Vivian");
    arrayList.add("Amy");
    arrayList.add("Angst");

    // 遍历元素
    arrayList.stream().forEach(System.out::println);
    
    // 返回的是一个可迭代对象
    System.out.println(arrayList.stream().iterator());
}

结果输出:


Vivian
Amy
Angst

java.util.Spliterators$1Adapter@7ba4f24f

Process finished with exit code 0
  • toArray(): 返回一个数组
  • collect(): 收集操作,将一个对象的集合转化成另一个对象的集合
// 把流转换成数组
Object[] toArray = Stream.of(1, 9, 5, 2, 7, -1).toArray();

// 使用系统提供的收集器可以将最终的数据流收集到 List Set Map等容器中
List list = Stream.of(1, 9, 5, 2, 7, -1).collect(Collectors.toList());

最后

以上就是本次的学习分享,Stream 的熟练使用可以让你的代码更加简洁易于维护,以上案例中可能还涉及其他的知识点,比如函数式接口、lambda等,感兴趣的可以看下笔者其他的学习笔记。欢迎留言讨论

你可能感兴趣的:(Java8 Stream 入门看这篇就够了)