深入 Java Stream API:利用流式操作对集合进行高效的过滤、映射、聚合,提升数据处理效率与可读性。

各位听众,各位码农,大家好!我是老码,今天咱们聊聊Java Stream API,这玩意儿就像魔法棒一样,能让你的集合操作变得高效又优雅。

前言:集合的烦恼,以及Stream的救赎

话说这程序员的世界里,跟集合打交道那是家常便饭。List、Set、Map,哪个不是咱们的老朋友?可是,当数据量一大,要对这些集合进行过滤、转换、聚合的时候,传统的循环遍历就显得笨重不堪,代码又臭又长,看得人眼花缭乱,心情烦躁,恨不得摔键盘!

想想看,你要从一个学生列表中找出所有年龄大于18岁的学生,然后提取他们的姓名,最后按照姓名排序… 传统的写法:

List<Student> students = ...; // 一堆学生
List<String> adultStudentNames = new ArrayList<>();

for (Student student : students) {
    if (student.getAge() > 18) {
        adultStudentNames.add(student.getName());
    }
}

Collections.sort(adultStudentNames);

这段代码是不是让你感觉回到了上个世纪? 就像在泥泞的道路上艰难跋涉,每一步都留下沉重的脚印。

这时,Stream API就像一位身披金甲的骑士,驾着风火轮来拯救我们了! 它以一种声明式的方式,让我们能专注于“做什么”,而不是“怎么做”。上面的例子用Stream API可以这样写:

List<String> adultStudentNames = students.stream()
                                       .filter(student -> student.getAge() > 18)
                                       .map(Student::getName)
                                       .sorted()
                                       .collect(Collectors.toList());

看到没?代码瞬间变得简洁明了,就像一首轻快的诗歌,优美而流畅。 🤩

Stream API是什么? 它的核心理念是什么?

Stream API是Java 8引入的一个强大的新特性,它允许你以一种声明式的方式处理数据集合。 它的核心理念是:

  • 声明式编程: 告诉程序 "做什么",而不是 "怎么做"。就像你告诉厨师 "我要一份宫保鸡丁",而不是告诉他 "先把鸡肉切丁,然后…,最后…”。
  • 链式操作: 像流水线一样,将多个操作串联起来,每个操作都对数据进行一次转换,最终得到想要的结果。
  • 延迟执行: Stream API中的很多操作都是惰性的,只有在需要结果的时候才会执行。这可以避免不必要的计算,提高效率。
  • 内部迭代: Stream API负责管理数据的迭代过程,你只需要专注于对数据的处理逻辑。就像你坐上了自动驾驶汽车,只需要告诉它 "去XX地方",而不用自己控制方向盘和油门。

Stream API的基本组成:三大金刚

Stream API由三个主要部分组成,我们称之为“三大金刚”:

  1. 数据源 (Source): 这是Stream的起点,可以是集合、数组、I/O通道等等。就像河流的源头,是整个流的起点。
  2. 中间操作 (Intermediate Operations): 这些操作会对Stream中的数据进行转换、过滤、排序等处理,返回一个新的Stream。这些操作可以链式调用,形成一个操作管道。就像河流中的水流,经过各种弯道、瀑布,最终到达目的地。
  3. 终端操作 (Terminal Operations): 这些操作会消费Stream,产生一个最终的结果,或者触发Stream的执行。就像河流的入海口,是整个流的终点。

用表格来总结一下:

操作类型 说明 示例 返回值
数据源 (Source) Stream的起点,提供数据。 List.stream(), Arrays.stream(array) Stream<T>
中间操作 对Stream中的数据进行转换、过滤、排序等处理,返回一个新的Stream。可以链式调用。 filter(predicate), map(function), sorted(), distinct(), limit(n), skip(n) Stream<T> (通常)
终端操作 消费Stream,产生一个最终的结果,或者触发Stream的执行。 forEach(consumer), collect(collector), reduce(identity, accumulator), count(), min(comparator), max(comparator), anyMatch(predicate), allMatch(predicate), noneMatch(predicate), findFirst(), findAny() 各种类型,取决于具体操作。 例如: void (forEach), List<T> (collect), Optional<T> (findFirst, findAny), long (count), boolean (anyMatch, allMatch, noneMatch), T (reduce)

中间操作:Stream的魔术师

中间操作是Stream API的核心,它们提供了各种各样的转换和过滤功能,就像魔术师一样,可以把数据变成你想要的样子。

  • filter(Predicate predicate): 过滤Stream中的元素,只保留满足条件的元素。 Predicate 是一个函数式接口,接受一个参数,返回一个boolean值。 就像一个筛子,把不符合条件的沙子过滤掉,只留下金子。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    List<Integer> evenNumbers = numbers.stream()
                                        .filter(number -> number % 2 == 0) // 过滤出偶数
                                        .collect(Collectors.toList());
    // evenNumbers: [2, 4, 6, 8, 10]
  • map(Function function): 将Stream中的每个元素转换成另一种类型。 Function 是一个函数式接口,接受一个参数,返回一个结果。 就像一个变形金刚,把汽车变成飞机。

    List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    List<Integer> nameLengths = names.stream()
                                      .map(String::length) // 获取每个字符串的长度
                                      .collect(Collectors.toList());
    // nameLengths: [5, 3, 7]
  • flatMap(Function<T, Stream> function): 将Stream中的每个元素转换成一个Stream,然后将所有的Stream合并成一个Stream。 就像一个拆箱专家,把所有的盒子都打开,把里面的东西都拿出来。

    List<List<Integer>> numbers = Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4, 5));
    List<Integer> flattenedNumbers = numbers.stream()
                                            .flatMap(List::stream) // 将List<List<Integer>> 转换为 List<Integer>
                                            .collect(Collectors.toList());
    // flattenedNumbers: [1, 2, 3, 4, 5]
  • sorted(): 对Stream中的元素进行排序。 可以使用默认的自然排序,也可以自定义排序规则。 就像一个整理大师,把杂乱无章的东西整理得井井有条。

    List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
    List<String> sortedNames = names.stream()
                                    .sorted() // 按照字母顺序排序
                                    .collect(Collectors.toList());
    // sortedNames: [Alice, Bob, Charlie]
    
    List<Student> students = ...; // 一堆学生
    List<Student> sortedStudents = students.stream()
                                         .sorted(Comparator.comparing(Student::getAge).reversed()) // 按照年龄降序排序
                                         .collect(Collectors.toList());
  • distinct(): 去除Stream中重复的元素。 就像一个去重神器,把重复的东西都扔掉,只留下独一无二的。

    List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 4, 4, 4);
    List<Integer> distinctNumbers = numbers.stream()
                                          .distinct() // 去除重复元素
                                          .collect(Collectors.toList());
    // distinctNumbers: [1, 2, 3, 4]
  • limit(long maxSize): 截取Stream中的前maxSize个元素。 就像一个剪刀手,把多余的部分剪掉,只留下精华。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    List<Integer> limitedNumbers = numbers.stream()
                                          .limit(5) // 截取前5个元素
                                          .collect(Collectors.toList());
    // limitedNumbers: [1, 2, 3, 4, 5]
  • skip(long n): 跳过Stream中的前n个元素。 就像一个跳远运动员,跳过前面的障碍,直接到达目的地。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    List<Integer> skippedNumbers = numbers.stream()
                                          .skip(5) // 跳过前5个元素
                                          .collect(Collectors.toList());
    // skippedNumbers: [6, 7, 8, 9, 10]

终端操作:Stream的终结者

终端操作会消费Stream,产生一个最终的结果,或者触发Stream的执行。 就像电影的结尾,给故事画上一个句号。

  • forEach(Consumer consumer): 对Stream中的每个元素执行一个操作。 Consumer 是一个函数式接口,接受一个参数,不返回任何值。 就像一个播音员,把每个人的名字都念一遍。

    List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    names.stream()
         .forEach(System.out::println); // 打印每个名字
  • collect(Collector collector): 将Stream中的元素收集到一个集合中。 Collector 是一个接口,提供了各种各样的收集方式,例如toList、toSet、toMap等等。 就像一个收纳盒,把所有的东西都放进去。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    List<Integer> evenNumbers = numbers.stream()
                                        .filter(number -> number % 2 == 0)
                                        .collect(Collectors.toList()); // 收集到List中
    
    Set<Integer> oddNumbers = numbers.stream()
                                       .filter(number -> number % 2 != 0)
                                       .collect(Collectors.toSet()); // 收集到Set中
    
    Map<String, Integer> nameLengthMap = names.stream()
                                               .collect(Collectors.toMap(name -> name, String::length)); // 收集到Map中
  • reduce(identity, accumulator): 将Stream中的元素归约为一个值。 identity 是初始值, accumulator 是一个函数式接口,接受两个参数,返回一个结果。 就像一个搅拌机,把所有的东西都混合在一起,变成一种新的东西。

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    int sum = numbers.stream()
                     .reduce(0, (a, b) -> a + b); // 求和
    // sum: 15
    
    String concatenatedNames = names.stream()
                                    .reduce("", (a, b) -> a + ", " + b); // 连接字符串
    // concatenatedNames: ", Alice, Bob, Charlie"
  • count(): 返回Stream中元素的个数。 就像一个计数器,统计有多少个东西。

    long count = numbers.stream()
                        .count(); // 统计元素的个数
    // count: 5
  • min(Comparator comparator): 返回Stream中最小的元素。 Comparator 是一个接口,用于比较两个元素的大小。 就像一个选美大赛,选出最漂亮的那个。

    Optional<Integer> min = numbers.stream()
                                   .min(Integer::compareTo); // 找到最小的元素
    // min: Optional[1]
  • max(Comparator comparator): 返回Stream中最大的元素。 就像一个大力士比赛,选出最强壮的那个。

    Optional<Integer> max = numbers.stream()
                                   .max(Integer::compareTo); // 找到最大的元素
    // max: Optional[5]
  • anyMatch(Predicate predicate): 判断Stream中是否存在任意一个元素满足条件。 就像一个侦探,寻找蛛丝马迹,只要找到一个就够了。

    boolean hasEvenNumber = numbers.stream()
                                   .anyMatch(number -> number % 2 == 0); // 是否存在偶数
    // hasEvenNumber: true
  • allMatch(Predicate predicate): 判断Stream中是否所有元素都满足条件。 就像一个考官,检查每个学生的答案,必须全部正确才行。

    boolean allPositive = numbers.stream()
                                 .allMatch(number -> number > 0); // 是否所有元素都大于0
    // allPositive: true
  • noneMatch(Predicate predicate): 判断Stream中是否没有元素满足条件。 就像一个医生,检查病人是否健康,必须没有任何疾病才行。

    boolean noNegative = numbers.stream()
                                .noneMatch(number -> number < 0); // 是否没有负数
    // noNegative: true
  • findFirst(): 返回Stream中的第一个元素。 就像一个寻宝者,找到第一个宝藏就满足了。

    Optional<Integer> first = numbers.stream()
                                    .findFirst(); // 找到第一个元素
    // first: Optional[1]
  • findAny(): 返回Stream中的任意一个元素。 在并行Stream中,可以更高效地找到一个元素。 就像一个随机抽奖,随便抽一个就行了。

    Optional<Integer> any = numbers.parallelStream() // 使用并行Stream
                                   .findAny(); // 找到任意一个元素
    // any: Optional[1] (结果不确定)

Stream的性能考量:并非银弹

Stream API 确实很强大,但它也不是万能的。在使用Stream API的时候,我们需要注意一些性能问题:

  • 中间操作的开销: 每个中间操作都会创建一个新的Stream,这会带来一定的开销。 因此,我们应该尽量减少中间操作的次数。
  • 并行Stream的线程安全: 在使用并行Stream的时候,我们需要注意线程安全问题。 如果Stream中的操作不是线程安全的,可能会导致数据竞争和错误。
  • 过度使用Stream: 虽然Stream API很方便,但是过度使用Stream可能会导致代码可读性下降。 在一些简单的场景下,传统的循环遍历可能更合适。

Stream API的实战案例:让代码飞起来

说了这么多理论,不如来几个实战案例,让大家感受一下Stream API的魅力。

案例1:统计学生成绩

假设我们有一个学生列表,每个学生都有姓名和成绩。 我们要统计所有及格学生的平均成绩。

class Student {
    private String name;
    private int score;

    // 省略构造函数、getter和setter
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setScore(int score) {
        this.score = score;
    }
}

List<Student> students = Arrays.asList(
    new Student("Alice", 80),
    new Student("Bob", 50),
    new Student("Charlie", 90),
    new Student("David", 60),
    new Student("Eve", 40)
);

double averageScore = students.stream()
                             .filter(student -> student.getScore() >= 60) // 过滤出及格的学生
                             .mapToInt(Student::getScore) // 获取成绩
                             .average() // 计算平均值
                             .orElse(0.0); // 如果没有及格的学生,返回0.0

System.out.println("及格学生的平均成绩:" + averageScore); // 输出:及格学生的平均成绩:76.66666666666667

案例2:查找最长的单词

假设我们有一个字符串列表,我们要找到其中最长的单词。

List<String> words = Arrays.asList("apple", "banana", "orange", "grapefruit");

Optional<String> longestWord = words.stream()
                                     .max(Comparator.comparingInt(String::length)); // 按照长度比较

System.out.println("最长的单词:" + longestWord.orElse("")); // 输出:最长的单词:grapefruit

案例3:统计单词出现次数

假设我们有一段文本,我们要统计每个单词出现的次数。

String text = "This is a test. This is only a test.";

Map<String, Long> wordCounts = Arrays.stream(text.split("\s+")) // 将文本分割成单词
                                      .map(word -> word.replaceAll("[^a-zA-Z]", "").toLowerCase()) // 去除标点符号并转换为小写
                                      .filter(word -> !word.isEmpty()) // 过滤掉空字符串
                                      .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); // 统计单词出现次数

System.out.println("单词出现次数:" + wordCounts); // 输出:单词出现次数:{a=2, this=2, test=2, is=2, only=1}

总结:Stream API,让你的代码更上一层楼

Stream API是Java 8中一个非常重要的特性,它能让我们以一种声明式的方式处理数据集合,提高代码的可读性和效率。 掌握Stream API,就像拥有了一把锋利的宝剑,能让你在代码的世界里披荆斩棘,所向披靡。 💪

希望今天的分享对大家有所帮助,谢谢大家! 🍻

最后的彩蛋:Stream API的进阶用法

  • 并行Stream: 利用多核CPU的优势,加速数据处理。
  • 自定义Collector: 实现更复杂的收集逻辑。
  • 无限Stream: 生成无限序列的数据。

这些高级用法,就留给大家自己去探索吧! 祝大家编码愉快! 😊

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注