Stream 概述

在 Java 8 中,新增了 Stream 这个重要的抽象概念,结合同版本出现的 Lambda 表达式,Stream 通过其 API 提供了一系列高效、友好的处理集合数据的操作方式。

从字面上理解,Stream 就是把集合中将要处理的元素集合看作一个流,通过使用其 API 对流中的元素进行各种操作,如过滤、映射、聚合等。它具有以下几个特性:

  1. Stream 只是一个抽象接口概念,其本身并不是一个数据结构,所以并不能用来存储数据。
  2. Stream API 不会改变数据源,所有操作的最终结果会保存到另外一个对象中。(peek 方法除外,它会修改流中的元素)。
  3. Stream 采用惰性计算的方式,流的中间过程只是记录了操作的步骤,并不会立即执行,在执行终端操作后才会进行实际的计算,返回一个新的对象。

Stream 的分类

Stream 的操作方式主要分为两大类,根据其执行特点还能继续向下细分,具体划分如下:

  • 中间操作(Intermediate operations):每次操作返回一个流,可以进行多次中间操作
  • 无状态操作(Stateless):可以直接对单个元素进行的操作,不受之前操作元素的影响。
  • 有状态操作(Stateful):只有获取到所有元素后才能执行的操作。
  • 终端操作(Terminal operations):每个流只能进行一个终端操作,执行后流无法再次使用,返回一个新的集合或者对象。
  • 非短路操作(non-short circuit):必须处理所有元素才能得到最终结果
  • 短路操作(short circuiting):遇到某些符合条件的元素就可以得到最终结果。

Stream 的创建

Stream 定义在 java.util.stream 包中,作为一个重要接口概念,在各种集合类以及数组类中都提供了初始化方法。Stream 自身也提供了一系列创建方法。

// 1.通过集合类的stream()或者parallelStream()方法
List<String> list = Arrays.asList("one", "two", "three");
Stream<String> stream = list.stream();
Stream<String> parallelStream = list.parallelStream();

// 2.通过数组的stream()方法
String array = {1,2,3,4,5};
IntStream stream = Arrays.stream(array);

// 3.通过Stream的静态方法
Stream<Character> charStream = Stream.of('a','b','c');
Stream<Integer> intStream = Stream.iterate(0, (x) -> x + 1).limit(5);
Stream<Double> doubleStream = Stream.generate(Math::random).limit(5);

stream是顺序流,而parallelStream是并行流,内部以多线程运行的方式对流进行操作,使用并行流时必须确保数据的处理对顺序是没有要求的。

在其他的场合,还有一些常见的使用 Stream 的方式,比如:

// 通过BufferedReader.lines()方法将每行的内容转换成流
BufferedReader reader = new BufferedReader(new FileReader("C:\\file.txt"));
Stream<String> lineStream = reader.lines();

// 通过Pattern.splitAsStream()方法将字符串转换成流
Pattern pattern = Pattern.compile(",");
Stream<String> stringStream = pattern.splitAsStream("a,b,c,d,e");

Stream 的使用

我们这里按照 Stream 操作的分类分别提供示例和进行讲解。

Stream 的中间操作

无状态操作
  • 筛选
  • filter:根据给定的条件过滤流中的某些元素。
  • distinct:通过流中元素的 hashCode() 和 equals() 方法去除重复元素。
Stream<Integer> stream = Stream.of(3,5,132,6,856,9,132,2,53);
Stream<Integer> newStream = stream.filter(s -> s > 10)  // 132, 856, 132, 53
    .distinct();  // 132, 856, 53
  • 切片
  • limit(n):获取 n 个元素。
  • skip(n):跳过 n 个元素,可以与 limit 配合实现分页。
Stream<Integer> stream = Stream.of(3,5,132,6,856,9,132,2,53);
Stream<Integer> newStream = stream.skip(4)  // 9, 132, 2, 53
    .limit(2); // 9, 132
  • 映射:将一个流的元素按照给定的规则映射到另一个流中。
  • map:接收一个函数作为参数,该函数会应用到每个元素上,将其映射长一个新的元素。
  • flatMap:接收一个函数作为参数,将流中的每个元素都转换成另一流,然后把所有的流连接成一个新的流。
  • 在上述两种映射的基础上,还有其他类似的、指定映射结果数据类型的映射操作。
List<String> list = Stream.of("a,b,c,d", "1,2,3,4");
// 去除每个字符串里面的逗号
Stream<String> s1 = list.stream().map(s -> s.replaceAll(",", ""));
// 将每个字符串按照逗号分割后转换成一个流,并将所有流组合起来
Stream<String> s2 = list.stream().flatMap(s -> {
    String[] split = s.split(",");
    Stream<String> newStream = Arrays.stream(split);
    return newStream;
});
  • 消费
  • peek:对流中的每个元素进行操作。与 map 类似,但 map 接收一个 Function 表达式,有返回值;而 peek 接收的是一个 Consumer 表达式,没有返回值,可能会修改流中元素的初始数据值。
User u1 = new User("John", "john@gmail.com");
User u2 = new User("Jenny", "jenny@qq.com");
List<User> list = Arrays.asList(u1, u2);
// 修改每个用户的邮箱信息为指定值,内置修改,无返回值
list.stream().peek(u -> u.setEmail("default@test.com"));
有状态操作
  • 排序(sorted)
  • sorted():自然排序,流中的元素需要实现Comparable接口。
  • sorted(Comparator c):定制排序,自定义Comparator排序器。
User u1 = new User("John", "john@gmail.com");
User u2 = new User("Jenny", "jenny@qq.com");
User u3 = new User("Zed", "zed@outlook.com");

// 自然排序,按照名字字母升序排列
list.stream().sorted();
// 自定义排序,按照名字字母倒序排列,名字相同时按照邮箱长度升序排序
list.stream().sorted((e1, e2) -> {
    if (e1.getName().equals(e2.getName())) {
        return e1.getEmail.length() - e2.getEmail.length();
    } else {
        return e2.getName.compareTo(e1.getName());
    }
})

Stream 的终端操作

短路操作:主要是一些匹配与提取结果操作。
  • allMatch:接收一个 Predicate 表达式,当流中每个元素都符合表达式的条件时返回 true,否则返回 false。
  • noneMatch:接收一个 Predicate 表达式,当流中的每个元素都不满足表达式的条件时返回 true,否则返回 false。
  • anyMatch:接收一个 Predicate 表达式,当流中出现某一个元素满足表达式的条件时返回 true,否则返回 false。
  • findFirst:返回流中的第一个元素。
  • findAny:返回流中的任意一个元素。
List<Integer> list = Arrays.asList(1,2,3,4,5,6);
 
boolean allMatch = list.stream().allMatch(e -> e > 2);  // false
boolean noneMatch = list.stream().noneMatch(e -> e > 10);  // true
boolean anyMatch = list.stream().anyMatch(e -> e > 5);   // true
 
Integer findFirst = list.stream().findFirst().get();  // 1
Integer findAny = list.stream().findAny().get();  // 4

在这里的 findFirstfindAny 方法返回的结果都是 Optional 对象


非短路操作:主要是一些对结果进行聚合、规约或者收集等操作。
  • 聚合
  • count:返回流中元素的总数。
  • max:返回流中元素的最大值。
  • min:返回流中元素的最小值。
List<Integer> list = Arrays.asList(1,2,3,4,5,6);

long count = list.stream().count();  // 6
Integer max = list.stream().max(Integer::compareTo).get();  //6
  • 收集(collect):收集一个 Collector 实例,将流中返回的元素收集成另外一个数据结构。

在收集中常常使用到 Collectors 工具库,通过其静态方法返回一个 Collector 实例。具体应用方式如下:

// 构造对象列表,构建函数入参分别为name,gender, email,age
User u1 = new User("John", "m", "john@gmail.com", 14);
User u2 = new User("Jenny", "f", "jenny@qq.com", 15);
User u3 = new User("Zed", "m", "zed@outlook.com", 27);
List<User> users = Arrays.asList(u1, u2, u3);

// 提取元素转换成list
List<Integer> ageList = users.stream().map(User::getAge).collect(Collectors.toList());
// 提取元素转换成set
Set<Integer> ageSet = users.stream().map(User::getAge).collect(Collectors.toSet());
// 提取元素转换成map(这里key不能重复)
Map<String, Integer> userMap = users.stream().collect(Collectors.toMap(User::getName, User::getAge));
// 提取元素转换成map(这里相同的key允许value新值覆盖旧值;若是(m, m0) -> m则为保留原来的值)
Map<String, Integer> userMap = users.stream().collect(Collectors.toMap(User::getName, User::getAge, (m, m0) -> m0));
// 对象去重
List<User> distinctUserList = users.stream().collect(Collectors.collectingAndThen(
	Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getName))),
	ArrayList::new));

// 字符串分隔符连接
String joinName = list.stream().map(Student::getName).collect(Collectors.joining(",", "(", ")"));

除了 stream( ) 自身的聚合方法,使用 Collector 实例同样可以进行聚合运算。

// 1.学生总数
Long count = list.stream().collect(Collectors.counting());
// 2.最大年龄 (最小的minBy同理)
Integer maxAge = list.stream().map(Student::getAge).collect(Collectors.maxBy(Integer::compare)).get();
// 3.所有人的年龄
Integer sumAge = list.stream().collect(Collectors.summingInt(Student::getAge));
// 4.平均年龄
Double averageAge = list.stream().collect(Collectors.averagingDouble(Student::getAge));
  • 分组:按照指定条件将 stream 划分为多个 map。
  • 分区:按照指定条件将 stream 划分为 true 和 false 两个 map。
//分组
Map<Integer, List<Student>> ageMap = list.stream().collect(Collectors.groupingBy(Student::getAge));
//多重分组,先根据性别分再根据年龄分
Map<Integer, Map<Integer, List<Student>>> typeAgeMap = list.stream().collect(Collectors.groupingBy(Student::getGender, Collectors.groupingBy(Student::getAge)));
 
//分区
//分成两部分,一部分大于10岁,一部分小于等于10岁
Map<Boolean, List<Student>> partMap = list.stream().collect(Collectors.partitioningBy(v -> v.getAge() > 10));
  • 归约(reduce):将流中的元素缩减成一个值进行返回,通常用于求和、求积等操作。
List<Integer> list = Arrays.asList(1, 3, 2, 8, 11, 4);

Optional<Integer> sum = list.stream().reduce((x, y) -> x + y);
Optional<Integer> sum2 = list.stream().reduce(Integer::sum);
Integer sum3 = list.stream().reduce(0, Integer::sum);

Optional<Integer> product = list.stream().reduce((x, y) -> x * y);

Optional<Integer> max = list.stream().reduce((x, y) -> x > y ? x : y);
Integer max2 = list.stream().reduce(1, Integer::max);

Integer allAge = list.stream().map(Student::getAge).collect(Collectors.reducing(Integer::sum)).get();

通过上面的例子可以看出,Collectors 工具类提供的 reducing 方法和 Stream 本身的 reduce 方法都实现了归约的操作,两者的主要差别在于,前者增加了对自定义归约方法的支持。