• 提高Java开发生产力,我选Stream API,真香啊


    Java 8 引入的Stream API提供了一种新的数据处理方式,它以声明式、函数式的编程模型,极大地简化了对集合、数组或其他支持数据源的操作。Stream可以被看作是一系列元素的流水线。允许你高效地对大量数据执行复杂的过滤、映射、排序、聚合等操作,而无需显式地使用循环或者临时变量。Stream API的设计理念主要包括两个方面:链式调用惰性求值。链式调用允许我们将多个操作连接在一起,形成一个流水线,而惰性求值意味着只有在真正需要结果的时候才执行计算,从而避免了不必要的计算开销。

    接下来我们就来盘点一下日常开发中常用的一些Stream API。

    创建Stream

    • 集合创建
    List list = new ArrayList<>(); 
    // 串行流
    Stream stream = list.stream();
    // 并行流
    Stream parallelStream = list.parallelStream();
    
    • 数组创建
    String[] strs = new String[3];  
    Stream stream = Arrays.stream(strs);
    
    • 使用Stream.of(T...values)创建
    Stream stream = Stream.of("Apple", "Orange", "Banana");
    
    • 使用Stream.generate()创建流
    // 生成一个无限流,通过limit()限制元素个数  
    Stream randomStream = Stream.generate(Math::random).limit(5);
    
    • 使用Stream.iterate()创建流
    // 生成一个等差数列,通过limit()限制元素个数 
    Stream integerStream = Stream.iterate(0, n -> n + 2).limit(5);
    
    • 使用IntStream、LongStream、DoubleStream创建原始类型流
    // 使用IntStream创建  
    IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]  
      
    // 使用LongStream创建  
    LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]
    

    IntStream我们使用的地方还是比较多的,比如我们按照下标遍历一个集合时,同常的做法是:for(int i = 0; i < list.size(); i++){},我们可以使用IntStream去改造一下,IntStream.rangeClosed(0, list.size()).forEach();

    中间操作

    中间操作是构建流水线的一部分,用于对流进行转换和处理,但它们并不会触发实际的计算。

    • 过滤操作(filter)
      过滤操作用于筛选流中的元素,保留满足指定条件的元素。Stream filter(Predicate predicate)filter接受一个谓词Predicate,我们可以通过这个谓词定义筛选条件,Predicate是一个函数式接口,其包含一个test(T t)方法,该方法返回boolean。
    private static void filterTest(){  
        List fruits = Arrays.asList("apple", "banana", "orange", "grape");  
        // 过滤长度大于5的水果  
        List filteredFruits = fruits.stream().filter(fruit -> fruit.length() > 5).collect(Collectors.toList());  
        System.out.println("长度大于5的水果: "+ filteredFruits);  
    }
    
    private static void filterTest(List students){  
        List filterStudents = students.stream()  
                .filter(student -> Objects.equals("武汉大学", student.getSchool()))  
                .collect(Collectors.toList());  
      
        filterStudents.forEach(System.out::println);  
    }
    

    打印结果:
    image.png

    • 映射操作(map/flatMap)
      映射操作用于对流中的每个元素进行转换。他有map以及flatMap两种操作。map就是基本的映射操作,对每个元素进行提取转换。
    // 将实体层映射成学生姓名字符串  
    List names = students.stream()  
            .map(Student::getName)  
            .collect(Collectors.toList());
    
    // 将字符串转大写。
    List upperList = Lists.newArrayList("hello", "world", "stream", "api").stream().map(String::toUpperCase).collect(Collectors.toList());
    

    日常开发中map操作我们用的非常多,比如数据库中查询出来的DO实体,我们需要转换为VO返回给前端页面展示,这时候我们可以使用map进行转换操作:

    List studentDOList = studentMapper.listStudents();
    
    List studentVOList = studentDOList.stream().map(studentDO -> {
    	StudentVO studentVO = StudentVO.builder().studentNo(studentDO.getId())
    	.studentName(studentDO.getName()).build();
    	return studentVO;
    }).collect(Collectors.toList());
    

    而flatMap的作用略微特殊,它用于将一个元素映射为一个流,然后将所有流连接成一个流。这在处理嵌套结构或集合中的元素是另一个集合的情况下非常有用。

    List> nestedWords = Arrays.asList(
        Arrays.asList("Java", "Kotlin"),
        Arrays.asList("Python", "Ruby"),
        Arrays.asList("JavaScript", "TypeScript")
    );
    
    // 使用 flatMap 将嵌套的 List 转换为一个扁平的 List, 结果将是包含所有单词的扁平流
    List wordList = nestedWords.stream()  
            .flatMap(List::stream).collect(Collectors.toList());
    
    System.out.println(wordList);
    
    // 打印结果: [Java, Kotlin, Python, Ruby, JavaScript, TypeScript]
    

    flatMap在使用时,通常会涉及到处理复杂的数据结构,比如处理嵌套的对象集合或者进行数据的扁平化。

    @Data
    @Builder
    class Student {  
        private String name;  
        private List grades;  
    }
    
    @Data
    @Builder
    class ClassRoom {  
        private List studentList;  
    }
    
    @Data
    @Builder
    class School {  
        private List classRoomList;  
    }
    
    School school = School.builder()  
            .classRoomList(Lists.newArrayList(  
                    ClassRoom.builder().studentList(Lists.newArrayList(  
                            Student.builder().name("Alice").gradeList(Lists.newArrayList(90, 85, 88)).build(),  
                                      Student.builder().name("Bob").gradeList(Lists.newArrayList(78, 92, 80)).build()  
                    )).build(),  
                    ClassRoom.builder().studentList(Lists.newArrayList(  
                            Student.builder().name("Charlie").gradeList(Lists.newArrayList(95, 89, 91)).build(),  
                            Student.builder().name("David").gradeList(Lists.newArrayList(82, 87, 79)).build()  
                    )).build()  
            ))  
            .build();  
      
    // 使用 flatMap 扁平化处理获取所有学生的所有课程成绩  
    List allGrades = school.getClassRoomList().stream()  
            .flatMap(classroom -> classroom.getStudentList().stream())  
            .flatMap(student -> student.getGradeList().stream())  
            .collect(Collectors.toList());  
      
    System.out.println(allGrades);
    // 打印结果:[90, 85, 88, 78, 92, 80, 95, 89, 91, 82, 87, 79]
    
    • mapToInt操作
      mapToInt 是 Stream API 中的一种映射操作,专门用于将元素映射为 IntStream。通过 mapToInt,你可以将流中的元素映射为 int 类型,从而进行更专门化的操作,例如数值计算。
    int totalAge2 = students.stream().mapToInt(Student::getAge).sum();
    

    类似的还有mapToLongmapToDouble 操作,这两个操作类似于 mapToInt,分别用于将流中的元素映射为 LongStreamDoubleStream

    • 排序操作(sorted)
      排序操作用于对流中的元素进行排序。
    List cities = Lists.newArrayList("New York", "Tokyo", "London", "Paris");
    
    // 对城市按字母顺序排序
    List sortedStream = cities.stream().sorted().collect(Collectors.toList());  
    

    对于集合中对象的排序,sorted要求待比较的元素必须实现Comparable接口。

    @Data  
    @Builder  
    static class Student implements Comparable{  
        private String name;  
        private Integer age;  
          
        @Override  
        public int compareTo(Student other) {  
            return other.getAge()-this.getAge();  
        }  
    }
    
    List sortedList = students.stream()  
            .sorted()  
    		.map(Student::getName()) 
            .collect(Collectors.toList());    
    

    如果没有实现,就需要将比较器作为参数传递给sorted(Comparator comparator)

    @Data  
    @Builder  
    static class Student {  
        private String name;  
        private Integer age;
    }
    
    List sortedList = students.stream()  
            .sorted((student1,student2) -> student2.getAge() - student1.getAge())  
            .map(Student::getName()) 
            .collect(Collectors.toList());    
    
    • 去重操作(distinct)
      去重操作用于去除流中的重复元素。distinct基于Object.equals(Object)实现。
    List numbers = Lists.newArrayList(1, 2, 3, 2, 4, 5, 3, 6);  
    // 去除重复的数字  
    List distinctList = numbers.stream().distinct().collect(Collectors.toList());
    
    // 或者去除学生中姓名相同的
    List studentNameList = students.stream()
    								.map(Student::getName()) 
    								.distinct()
    						        .collect(Collectors.toList());    
    
    
    • 截断操作(limit)
      截断操作用于限制流中元素的数量。limit返回包含前n个元素的流,当集合大小小于n时,则返回实际长度。
    List numbers = Lists.newArrayList(1, 2, 3, 2, 4, 5, 3, 6); 
    // 只取前三个数字 
    List limitedList = numbers.stream().limit(3).collect(Collectors.toList());
    
    // 取土工工程专业的年龄最小的前两名学生
    List limitStu = students.stream()  
            .filter(student -> Objects.equals("土木工程", student.getMajor())) 
            .sorted((student1,student2) -> student2.getAge() - student1.getAge())  
            .limit(2)  
            .collect(Collectors.toList());
    
    • 跳过操作(skip)
      跳过操作用于跳过流中的前几个元素,返回由后面所有元素构造的流,如果n大于满足条件的集合的长度,则会返回一个空的集合。作用上跟limit相反。
    List numbers = Lists.newArrayList(1, 2, 3, 2, 4, 5, 3, 6); 
    // 跳过前三个数字,返回后面的数字 
    List limitedList = numbers.stream().skip(3).collect(Collectors.toList());
    
    // 跳过土工工程专业的年龄最小的前两名学生,取后面的学生
    List limitStu = students.stream()  
            .filter(student -> Objects.equals("土木工程", student.getMajor())) 
            .sorted((student1,student2) -> student2.getAge() - student1.getAge())  
            .skip(2)  
            .collect(Collectors.toList());
    
    • peek操作
      peek 方法对每个元素执行操作并返回一个新的 Stream。peek 的主要目的是用于调试和观察流中的元素,通常用于打印调试信息、记录日志或其他类似的目的,而不会改变流中元素的结构。
    List words = Arrays.asList("apple", "banana", "orange", "grape");  
      
    List modifiedWords = words.stream()  
            .filter(word -> word.length() > 5)  
            .peek(word -> System.out.println("Filtered Word: " + word))  
            .map(String::toUpperCase)  
            .peek(word -> System.out.println("Uppercase Word: " + word))  
            .collect(Collectors.toList());
    

    Stream的终端操作

    终端操作是对流进行最终计算的操作,执行终端操作后,流将被消耗,不能再被使用。

    • 迭代forEach操作
      forEach 迭代操作,用于对流中的每个元素执行指定的操作。
    List fruits = Arrays.asList("apple", "banana", "orange", "grape");
    
    // 使用 forEach 输出每个水果
    fruits.stream().forEach(fruit -> System.out.println(fruit));
    // 执行forEach时可省略 stream(),即
    fruits.forEach(fruit -> System.out.println(fruit));
    // 或
    fruits.stream().forEach(System.out::println);
    
    • 收集操作(collect)
      通过collect()方法结合java.util.stream.Collectors工具类将Stream转换为另一种形式,例如列表、集合(toList, toSet, toMap)、映射或归约结果。如上述示例中的:
    1. 收集到List
      使用Collectors.toList()
    // 跳过土工工程专业的年龄最小的前两名学生,取后面的学生
    List limitStu = students.stream()  
            .filter(student -> Objects.equals("土木工程", student.getMajor())) 
            .sorted((student1,student2) -> student2.getAge() - student1.getAge())  
            .skip(2)  
            .collect(Collectors.toList());
    
    1. 收集到Set
      使用Collectors.toSet()
    // 将学生姓名收集到Set
    Set studentNameSet = students.stream().map(Student::getName)
    		.collect(Collectors.toSet());
    
    1. List转Map
      使用Collectors.toMap。日常开发中使用很多。
    // 转换为年龄对应的学生信息  
    Map studentMap = students.stream().collect(Collectors.toMap(
    											Student::getAge, 
    											Function.identity(), 
    											(e1,e2) -> e1));
    

    这段代码代表,我们使用年龄作为Map的key,对应学生信息作为value。Function.identity():这是一个提取元素自身的映射函数。(e1, e2) -> e1:这是一个合并冲突的操作。如果在流中存在相同的年龄(相同的键),这个函数定义了当出现重复键时应该如何处理。在这里,我们选择保留第一个出现的元素,即保留先出现的 Student 对象。当然我们还可以这样(e1, e2) -> {...}自定义合并冲突策略,例如:

    // 转换为年龄对应的学生信息,如果年龄相同,则取名字较长的  
    Map studentMap = students.stream().collect(Collectors.toMap(Student::getAge, Function.identity(), (e1,e2) -> {  
        return e1.getName().length() > e2.getName().length() ? e1 : e2;  
    }));
    

    如果value的值是一些number,我们也可以做一些加减乘除之类的合并。

    日常开发中,这个用法很频繁。

    1. 字符串拼接:
      使用Collectors.joining(拼接符)
    List students  = Lists.newArrayList(  
            Student.builder().name("Alice").gradeList(Lists.newArrayList(90, 85, 88)).build(),  
            Student.builder().name("Bob").gradeList(Lists.newArrayList(78, 92, 80)).build()  
    );  
      
    String studentName = students.stream().map(Student::getName).collect(Collectors.joining(","));
    
    // 打印出来:Alice,Bob
    
    1. 分组
      即按照集合中的元素的某个属性进行分组,转换为Map>:
    List fruits = Arrays.asList("apple", "banana", "orange", "grape");  
    Map> lengthToNamesMap = fruits.stream()  
                        .collect(Collectors.groupingBy(String::length));
    
    // 按照年龄分组  
    Map> studentMap = students.stream().collect(Collectors.groupingBy(Student::getAge));
    
    // 连续进行分组
    Map>> groupsStudent = students.stream()  
            // 先按照学校分组  
            .collect(Collectors.groupingBy(Student::getSchool  
            // 再按照专业分组  
            ,Collectors.groupingBy(Student::getMajor)));
    
    1. counting()
      counting() 收集器用于计算流中元素的数量。等同于Stream的count()操作。
    long studentCount = students.stream().collect(Collectors.counting());
    // 效果同等于
    long studentCount = students.stream().count();
    
    1. maxBy()
      maxBy()基于指定的比较器,用于找到流中的最大的元素。等同于Stream的max操作
    // 年龄最大的学生
    Student olderStudent = students.stream()  
            .collect(Collectors.maxBy((s1,s2) -> s1.getAge()- s2.getAge())).orElse(null);
    
    Student olderStudent2 = students.stream()  
        .collect(Collectors.maxBy(Comparator.comparing(Student::getAge))).orElse(null);
    
    // 等价于stram的max
    Student olderStudent = students.stream()
    	.max(Comparator.comparing(Student::getAge)).orElse(null);    
    
    1. minBy()
      minBy()基于指定的比较器,用于找到流中的最小的元素。等同于Stream的min操作。
    // 年龄最小的学生
    Student youngStudent = students.stream()  
        .collect(Collectors.minBy(Comparator.comparing(Student::getAge))).orElse(null); 
    
    Student youngStudent = students.stream()
    	.min(Comparator.comparing(Student::getAge)).orElse(null);
    
    1. averagingInt
      averagingInt() 收集器用于计算流中元素的平均值。
    // 求学生平均年龄
    double avgAge = students.stream()  
            .collect(Collectors.averagingInt(Student::getAge));
    
    1. summarizingInt()
      summarizingInt() 收集器用于计算流中元素的汇总统计信息,包括总数、平均值、最大值和最小值。
    // 一次性得到元素个数、总和、均值、最大值、最小值
    IntSummaryStatistics summaryStatistics = students.stream().collect(Collectors.summarizingInt(Student::getAge));
    
    System.out.println("总数:" + summaryStatistics.getCount()); 
    System.out.println("平均值:" + summaryStatistics.getAverage()); 
    System.out.println("最大值:" + summaryStatistics.getMax()); 
    System.out.println("最小值:" + summaryStatistics.getMin());
    
    • partitioningBy()
      将流中的元素按照指定的条件分成两个部分。在分区中key只有两种情况:true或false,目的是将待分区集合按照条件一分为二,分区相对分组的优势在于,我们可以同时得到两类结果,在一些应用场景下可以一步得到我们需要的所有结果,比如将数组分为奇数和偶数。
    // 分为武汉大学学生,非武汉大学学生
    Map> partStudent = students.stream()  
            .collect(Collectors.partitioningBy(student -> Objects.equals("武汉大学",student.getSchool())));
    
    • count操作
      count 用于计算流中的元素个数。效果等同于Collectors.counting()
    long studentCount = students.stream().count();
    // 效果同等于
    long studentCount = students.stream().collect(Collectors.counting());
    
    
    • max操作
      基于指定比较器,max用于找到流中最大的元素。效果等同于Collectors.maxBy()
    Student olderStudent = students.stream()
    	.max(Comparator.comparing(Student::getAge)).orElse(null);  
    
    Student olderStudent2 = students.stream()  
        .collect(Collectors.maxBy(Comparator.comparing(Student::getAge))).orElse(null);
    
    
    • min操作
      基于指定比较器,min用于找到流中最小的元素。效果等同于Collectors.minBy()
    Student youngStudent = students.stream()
    	.min(Comparator.comparing(Student::getAge)).orElse(null);
    	
    // 年龄最小的学生
    Student youngStudent = students.stream()  
        .collect(Collectors.minBy(Comparator.comparing(Student::getAge))).orElse(null); 
    
    
    • reduce操作
      reduce 用于对流中的元素进行归约操作,得到一个最终的结果。
    // 计算学生的总年龄
    int totalAge1 = students.stream()  
            .map(Student::getAge)  
            .reduce(0, (a,b) -> a+b);
    
    // 也可以使用Integer.sum
    int totalAge2 = students.stream() 
            .map(Student::getAge)  
            .reduce(0, Integer::sum);
    
    // 也可以不设置初始值0,直接Integer.sum,但是返回的是Optional
    int totalAge3 = students.stream()  
           .map(Student::getAge)  
           .reduce(Integer::sum).orElse(0);
    
    • findFirst操作
      findFirst 用于查找流中的第一个元素。也即list.get(0)
    Student firstStu = students.stream()  
            .filter(student -> Objects.equals("土木工程", student.getMajor()))  
            .findFirst().orElse(null);
            
    

    曾经有个小兄弟问我,他有一段代码类似 Student firstStu = students.get(0)。他们组长让他优化优化,然后就用了这种方式优化的。😂

    • findAny操作
      findAny 用于查找流中的任意一个元素。在并行流中,findAny 可以更快地获取结果,而在串行流中与 findFirst 的效果基本一致。
    Student anyStu = students.stream()  
            .filter(student ->Objects.equals("土木工程", student.getMajor()))  
            .findAny().orElse(null);
    
    • anyMatch操作
      anyMatch则是检测是否存在一个或多个满足指定的参数行为,如果满足则返回true。
    boolean hasQh = students.stream()
    		.anyMatch(student -> Objects.equals("清华大学", student.getSchool()));
    
    • noneMatch
      noneMatch用于检测是否不存在满足指定行为的元素,如果不存在则返回true.
    boolean hasBd = students.stream()  
            .noneMatch(student -> Objects.equals("北京大学", student.getSchool()));
    
    • allMatch
      allMatch用于检测是否全部都满足指定的参数行为,如果全部满足则返回true。
    boolean isAdult = students.stream()  
            .allMatch(student -> student.getAge() > 18);
    

    并行流

    在Java 8及以上版本,你可以使用并行流(Parallel Stream)来充分利用多核处理器的能力。并行流在处理大量数据时可以提高性能,但并不是在所有情况下都比顺序流更快。当在并行流上进行操作时,需要注意并发问题。确保你的操作是无状态的、无副作用的,或者使用合适的并发工具。一定一定要注意线程安全。并行流本质上基于java7的Fork-Join框架实现,其默认的线程数为宿主机的内核数。

    创建并行流,只需要将stream()替换成parallelStream()即可。

    List list = studentMapper.listStudents();
    Stream parallelStream = students.parallelStream();
    

    与顺序流相似,你可以在并行流上执行各种中间和终端操作。

    日常中,对于大批量的数据处理转换,我们可以使用并行流去处理。我们可以先把数据切分成100或者其他数值一组的List> 然后使用并行流去处理这些数据。

    List studentVOList = Collections.synchronizedList(Lists.newArrayList());  
    Lists.partition(students, 100).parallelStream().forEach(pList -> {
    	// 处理转换数据
        List voList = convertList(pList);  
        studentVOList.addAll(voList);  
    });
    

    再比如一些大批量的数据分批次查询,都可以使用并行流去做,但是一定要注意线程安全。

    注意事项

    使用Stream API可使Java集合处理更简洁、清晰,充分发挥现代、函数式编程的优势。然而,需注意Stream的惰性求值,只在终端操作触发时执行中间操作,确保操作的必要性,避免不必要计算。Stream鼓励无状态、无副作用的操作,避免在中间操作修改共享状态,以确保流的预测性和可维护性。Stream不可重用,一旦被消费,无法再次使用,需谨慎设计流程。并行流虽提高性能,但需谨慎使用,不适用于所有情况,可能导致额外性能开销。

    总结

    Java 8中引入的Stream API为开发者带来了全新的编程范式。其链式调用和惰性求值的设计理念,使得数据处理变得更为简单和高效。通过深入理解Stream API,我们能够更好地利用这一强大工具,在实际开发中写出更为优雅和易读的代码。

    本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

  • 相关阅读:
    【vscode使用clang-format格式化代码】
    OPC DCOM快速配置
    CAS:111790-37-5_Biotin-NH2_生物素-氨基 生物素化化合物
    递归回溯实战+思想
    230. Kth Smallest Element in a BST
    OpenAI Sora文本生成视频注册教程
    网络安全(黑客)自学
    java基于微信小程序的驾校预约管理系统+ssm+uinapp+Mysql+计算机毕业设计
    Hadoop快速上手-1
    为了摆脱 Android ,亚马逊开发基于Linux的操作系统
  • 原文地址:https://www.cnblogs.com/coderacademy/p/18020463