前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【后端】Stream 常用操作,简化你的代码!

【后端】Stream 常用操作,简化你的代码!

作者头像
框架师
发布2025-01-15 09:22:39
发布2025-01-15 09:22:39
8600
代码可运行
举报
文章被收录于专栏:墨白的Java基地墨白的Java基地
运行总次数:0
代码可运行

Java8 由 Oracle 在 2014 年发布,是继 Java5 之后最具革命性的版本了。 Java8 吸收其他语言的精髓带来了函数式编程,lambda 表达式,Stream 流等一系列新特性,学会了这些新特性,可以让你实现高效编码优雅编码。

1. Stream 是什么?

  • Stream 是 Java8 新增的一个接口,允许以声明性方式处理数据集合。Stream 不是一个集合类型不保存数据,可以把它看作是遍历数据集合的高级迭代器(Iterator)。
  • Stream 操作可以像 Builder 一样逐步叠加,形成一条流水线。流水线一般由数据源 + 零或者多个中间操作 + 一个终端操作所构成。中间操作可以将流转换成另外一个流,比如使用 filter 过滤元素,使用 map 映射提取值。
  • Stream 与 lambda 表达式密不可分,本文默认你已经掌握了 lambda 基础知识。

2. Stream 的特点

  • 只能遍历(消费)一次。Stream 实例只能遍历一次,终端操作后一次遍历就结束,再次遍历需要重新生成实例,这一点类似于 Iterator 迭代器。
  • 保护数据源。对 Stream 中任何元素的修改都不会导致数据源被修改,比如过滤删除流中的一个元素,再次遍历该数据源依然可以获取该元素。
  • 懒。filter, map 操作串联起来形成一系列中间运算,如果没有一个终端操作(如 collect)这些中间运算永远也不会被执行。

3. 创建 Stream 实例的方法

  1. 使用指定值创建 Stream 实例
代码语言:javascript
代码运行次数:0
运行
复制
// of 为 Stream 的静态方法
Stream<String> strStream = Stream.of("hello", "java8", "stream");
// 或者使用基本类型流
IntStream intStream = IntStream.of(1, 2, 3);
  1. 使用集合创建 Stream 实例(常用方式)
代码语言:javascript
代码运行次数:0
运行
复制
// 使用 guava 库,初始化一个不可变的 list 对象
ImmutableList<Integer> integers = ImmutableList.of(1, 2, 3);
// List 接口继承 Collection 接口,java8 在 Collection 接口中添加了 stream 方法
Stream<Integer> stream = integers.stream();
  1. 使用数组创建 Stream 实例
代码语言:javascript
代码运行次数:0
运行
复制
// 初始化一个数组
Integer[] array = {1, 2, 3};
// 使用 Arrays 的静态方法 stream
Stream<Integer> stream = Arrays.stream(array);
  1. 使用生成器创建 Stream 实例
代码语言:javascript
代码运行次数:0
运行
复制
// 随机生成 100 个整数
Random random = new Random();
// 加上 limit 否则就是无限流了
Stream<Integer> stream = Stream.generate(random::nextInt).limit(100);
  1. 使用迭代器创建 Stream 实例
代码语言:javascript
代码运行次数:0
运行
复制
// 生成 100 个奇数,加上 limit 否则就是无限流了
Stream<Integer> stream = Stream.iterate(1, n -> n + 2).limit(100);
stream.forEach(System.out::println);
  1. 使用 IO 接口创建 Stream 实例
代码语言:javascript
代码运行次数:0
运行
复制
// 获取指定路径下文件信息,list 方法返回 Stream 类型
Stream<Path> pathStream = Files.list(Paths.get("/"));

4. Stream 常用操作

Stream 接口中定义了很多操作,大致可以分为两大类,一类是中间操作,另一类是终端操作;

1. 中间操作

  • 中间操作会返回另外一个流,多个中间操作可以连接起来形成一个查询。
  • 中间操作有惰性,如果流上没有一个终端操作,那么中间操作是不会做任何处理的。

下面介绍常用的中间操作:

map 操作

map 是将输入流中每一个元素映射为另一个元素形成输出流。

代码语言:javascript
代码运行次数:0
运行
复制
// 初始化一个不可变字符串
List<String> words = ImmutableList.of("hello", "java8", "stream");
// 计算列表中每个单词的长度
List<Integer> list = words.stream()
        .map(String::length)
        .collect(Collectors.toList());
// output: 5 5 6
list.forEach(System.out::println);

flatMap 操作

代码语言:javascript
代码运行次数:0
运行
复制
List<String[]> list1 = words.stream()
        .map(word -> word.split("-"))
        .collect(Collectors.toList());
        
// output: [Ljava.lang.String;@59f95c5d, 
//             [Ljava.lang.String;@5ccd43c2
list1.forEach(System.out::println);

纳里?你预期是 List, 返回却是 List<String[]>, 这是因为 split 方法返回的是 String[]

这个时候你可以想到要将数组转成 stream, 于是有了第二个版本

代码语言:javascript
代码运行次数:0
运行
复制
Stream<Stream<String>> arrStream = words.stream()
        .map(word -> word.split("-"))
        .map(Arrays::stream);
        
// output: java.util.stream.ReferencePipeline$Head@2c13da15, 
// java.util.stream.ReferencePipeline$Head@77556fd
arrStream.forEach(System.out::println);

还是不对,这个问题使用 flatMap 扁平流可以解决,flatMap 将流中每个元素取出来转成另外一个输出流

代码语言:javascript
代码运行次数:0
运行
复制
Stream<String> strStream = words.stream()
        .map(word -> word.split("-"))
        .flatMap(Arrays::stream)
        .distinct();
// output: hello java8 stream
strStream.forEach(System.out::println);

filter 操作

filter 接收 Predicate 对象,按条件过滤,符合条件的元素生成另外一个流。

代码语言:javascript
代码运行次数:0
运行
复制
// 过滤出单词长度大于 5 的单词,并打印出来
List<String> words = ImmutableList.of("hello", "java8", "hello", "stream");
words.stream()
        .filter(word -> word.length() > 5)
        .collect(Collectors.toList())
        .forEach(System.out::println);
// output: stream

2. 终端操作

终端操作将 stream 流转成具体的返回值,比如 List,Integer 等。常见的终端操作有:foreach, min, max, count 等。

foreach 很常见了,下面举一个 max 的例子。

代码语言:javascript
代码运行次数:0
运行
复制
// 找出最大的值
List<Integer> integers = Arrays.asList(6, 20, 19);
integers.stream()
        .max(Integer::compareTo)
        .ifPresent(System.out::println);
// output: 20

5. 实战:使用 Stream 重构老代码

假如有一个需求:过滤出年龄大于 20 岁并且分数大于 95 的学生。

使用 for 循环写法:

代码语言:javascript
代码运行次数:0
运行
复制
private List<Student> getStudents() {
    Student s1 = new Student("xiaoli", 18, 95);
    Student s2 = new Student("xiaoming", 21, 100);
    Student s3 = new Student("xiaohua", 19, 98);
    List<Student> studentList = Lists.newArrayList();
    studentList.add(s1);
    studentList.add(s2);
    studentList.add(s3);
    return studentList;
}
public void refactorBefore() {
    List<Student> studentList = getStudents();
    // 使用临时 list
    List<Student> resultList = Lists.newArrayList();
    for (Student s : studentList) {
        if (s.getAge() > 20 && s.getScore() > 95) {
            resultList.add(s);
        }
    }
    // output: Student{name=xiaoming, age=21, score=100}
    resultList.forEach(System.out::println);
}

使用 for 循环会初始化一个临时 list 用来存放最终的结果,整体看起来不够优雅和简洁。

使用 lambda 和 stream 重构后:

代码语言:javascript
代码运行次数:0
运行
复制
public void refactorAfter() {
    List<Student> studentLists = getStudents();
    // output: Student{name=xiaoming, age=21, score=100}
   studentLists.stream().filter(this::filterStudents).forEach(System.out::println);
}
private boolean filterStudents(Student student) {
    // 过滤出年龄大于 20 岁并且分数大于 95 的学生
    return student.getAge() > 20 && student.getScore() > 95;
}

使用 filter 和方法引用使代码清晰明了,也不用声明一个临时 list,非常方便。

6. 使用 Stream 常见的误区

  1. 误区一:重复消费 stream 对象

stream 对象一旦被消费,不能再次重复消费。

代码语言:javascript
代码运行次数:0
运行
复制
List<String> strings = Arrays.asList("hello", "java8", "stream");
Stream<String> stream = strings.stream();
stream.forEach(System.out::println); // ok
stream.forEach(System.out::println); // IllegalStateException

上述代码执行后报错:

java.lang.IllegalStateException: stream has already been operated upon or closed

  1. 误区二:修改数据源

在流操作的过程中尝试添加新的 string 对象,结果报错:

代码语言:javascript
代码运行次数:0
运行
复制
List<String> strings = Arrays.asList("hello", "java8", "stream");
// expect: HELLO JAVA8 STREAM WORLD, but throw UnsupportedOperationException
strings.stream()
        .map(s -> {
            strings.add("world");
            return s.toUpperCase();
        }).forEach(System.out::println);

注意:一定不要在操作流的过程中修改数据源。

总结

java8 流式编程在一定程度上可以使代码变得优美,不过也要避开常见的坑,如:不要重复消费对象、不要修改数据源。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-01-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. Stream 是什么?
  • 2. Stream 的特点
  • 3. 创建 Stream 实例的方法
  • 4. Stream 常用操作
  • 5. 实战:使用 Stream 重构老代码
  • 6. 使用 Stream 常见的误区
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档