【JDK8新特性】Stream流API上Day4

写在前面

欢迎来到JDK8新特性系列教程的第四天!在前面的学习中,我们已经掌握了Lambda表达式、函数式接口和方法引用。今天,我们将学习JDK8中最强大、最实用的特性------Stream API

Stream API可以说是JDK8给Java程序员带来的最大礼物。它彻底改变了我们处理集合数据的方式,让复杂的数据操作变得简洁、优雅且高效。如果你还在使用传统的for循环处理集合数据,那么今天的内容将为你打开一扇新的大门。

准备好了吗?让我们一起探索Stream API的强大功能!


目录

    • 写在前面
    • [一、为什么需要Stream API](#一、为什么需要Stream API)
      • [1.1 传统集合操作的痛点](#1.1 传统集合操作的痛点)
      • [1.2 Stream API的优势](#1.2 Stream API的优势)
      • [1.3 什么是Stream](#1.3 什么是Stream)
    • 二、Stream的创建方式
      • [2.1 从集合创建](#2.1 从集合创建)
      • [2.2 从数组创建](#2.2 从数组创建)
      • [2.3 使用Stream.of创建](#2.3 使用Stream.of创建)
      • [2.4 使用Stream.generate和Stream.iterate创建无限流](#2.4 使用Stream.generate和Stream.iterate创建无限流)
      • [2.5 从I/O和其他源创建](#2.5 从I/O和其他源创建)
      • [2.6 创建方式对比表](#2.6 创建方式对比表)
    • 三、Stream的中间操作
      • [3.1 filter - 过滤](#3.1 filter - 过滤)
      • [3.2 map - 映射](#3.2 map - 映射)
      • [3.3 flatMap - 扁平化映射](#3.3 flatMap - 扁平化映射)
      • [3.4 distinct - 去重](#3.4 distinct - 去重)
      • [3.5 sorted - 排序](#3.5 sorted - 排序)
      • [3.6 peek - 查看元素](#3.6 peek - 查看元素)
      • [3.7 limit和skip - 截取](#3.7 limit和skip - 截取)
      • [3.8 中间操作对比表](#3.8 中间操作对比表)
    • 四、Stream的终止操作
      • [4.1 forEach - 遍历](#4.1 forEach - 遍历)
      • [4.2 collect - 收集](#4.2 collect - 收集)
      • [4.3 toArray - 转换为数组](#4.3 toArray - 转换为数组)
      • [4.4 reduce - 归约](#4.4 reduce - 归约)
      • [4.5 min/max/count - 统计](#4.5 min/max/count - 统计)
      • [4.6 匹配操作](#4.6 匹配操作)
      • [4.7 findFirst/findAny - 查找](#4.7 findFirst/findAny - 查找)
      • [4.8 终止操作对比表](#4.8 终止操作对比表)
    • 五、经验之谈:Stream操作是惰性求值的
      • [5.1 什么是惰性求值](#5.1 什么是惰性求值)
      • [5.2 短路操作的优势](#5.2 短路操作的优势)
      • [5.3 短路操作列表](#5.3 短路操作列表)
    • 六、踩坑提醒
      • [6.1 Stream只能消费一次](#6.1 Stream只能消费一次)
      • [6.2 不要修改源数据](#6.2 不要修改源数据)
      • [6.3 注意空指针异常](#6.3 注意空指针异常)
      • [6.4 并行流的线程安全问题](#6.4 并行流的线程安全问题)
    • 七、面试高频考点
      • [7.1 Stream和Collection的区别?](#7.1 Stream和Collection的区别?)
      • [7.2 Stream的惰性求值是什么意思?](#7.2 Stream的惰性求值是什么意思?)
      • [7.3 什么时候使用并行流?](#7.3 什么时候使用并行流?)
    • 八、总结
    • 参考资料
    • 互动话题

一、为什么需要Stream API

1.1 传统集合操作的痛点

在没有Stream API之前,处理集合数据通常需要编写大量的样板代码:

java 复制代码
import java.util.*;

public class TraditionalApproach {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        // 需求:获取所有偶数,平方后排序,取前3个
        
        // 传统方式 - 代码冗长、可读性差
        List<Integer> evenNumbers = new ArrayList<>();
        for (Integer num : numbers) {
            if (num % 2 == 0) {
                evenNumbers.add(num);
            }
        }
        
        List<Integer> squared = new ArrayList<>();
        for (Integer num : evenNumbers) {
            squared.add(num * num);
        }
        
        Collections.sort(squared);
        
        List<Integer> result = new ArrayList<>();
        for (int i = 0; i < Math.min(3, squared.size()); i++) {
            result.add(squared.get(i));
        }
        
        System.out.println(result); // 输出: [4, 16, 36]
    }
}

传统方式的问题

  1. 代码冗长:需要多行代码完成简单操作
  2. 可读性差:业务逻辑被淹没在循环和条件判断中
  3. 容易出错:需要手动管理索引和临时集合
  4. 难以并行化:并行处理需要额外的线程管理代码

1.2 Stream API的优势

使用Stream API,同样的需求可以简洁地实现:

java 复制代码
import java.util.*;
import java.util.stream.Collectors;

public class StreamApproach {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        // Stream方式 - 声明式、简洁、可读性强
        List<Integer> result = numbers.stream()
            .filter(n -> n % 2 == 0)      // 筛选偶数
            .map(n -> n * n)               // 平方
            .sorted()                      // 排序
            .limit(3)                      // 取前3个
            .collect(Collectors.toList()); // 收集结果
        
        System.out.println(result); // 输出: [4, 16, 36]
    }
}

Stream API的优势

特性 传统方式 Stream API
代码风格 命令式(How) 声明式(What)
代码量
可读性
可维护性
并行支持 需手动实现 一行代码切换
函数式支持 完整支持

1.3 什么是Stream

**Stream(流)**是JDK8中引入的一个新的抽象层,它允许你以声明式的方式处理数据集合。Stream不是数据结构,而是对数据源(集合、数组、I/O等)的抽象视图。

Stream的特点

  1. 不存储数据:Stream本身不存储元素,只是对数据源的视图
  2. 不改变源数据:Stream操作不会修改原始数据源
  3. 惰性求值:中间操作不会立即执行,直到遇到终止操作
  4. 一次消费:Stream只能被消费一次,之后需要重新创建
  5. 支持并行:可以方便地切换串行/并行执行

二、Stream的创建方式

2.1 从集合创建

java 复制代码
import java.util.*;
import java.util.stream.Stream;

public class CreateFromCollection {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("a", "b", "c");
        Set<Integer> set = new HashSet<>(Arrays.asList(1, 2, 3));
        
        // List创建Stream
        Stream<String> listStream = list.stream();
        Stream<String> listParallelStream = list.parallelStream(); // 并行流
        
        // Set创建Stream
        Stream<Integer> setStream = set.stream();
        
        // 使用示例
        listStream.forEach(System.out::println);
        // 输出: a b c
    }
}

2.2 从数组创建

java 复制代码
import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.DoubleStream;
import java.util.stream.Stream;

public class CreateFromArray {
    public static void main(String[] args) {
        // 对象数组
        String[] strArray = {"a", "b", "c"};
        Stream<String> strStream = Arrays.stream(strArray);
        Stream<String> strStream2 = Stream.of(strArray);
        
        // 基本类型数组 - 使用特化Stream避免装箱
        int[] intArray = {1, 2, 3, 4, 5};
        IntStream intStream = Arrays.stream(intArray);
        
        long[] longArray = {1L, 2L, 3L};
        LongStream longStream = Arrays.stream(longArray);
        
        double[] doubleArray = {1.0, 2.0, 3.0};
        DoubleStream doubleStream = Arrays.stream(doubleArray);
        
        // 使用示例
        int sum = intStream.sum();
        System.out.println(sum); // 输出: 15
    }
}

2.3 使用Stream.of创建

java 复制代码
import java.util.stream.Stream;

public class CreateWithStreamOf {
    public static void main(String[] args) {
        // 直接传入元素
        Stream<String> stream1 = Stream.of("a", "b", "c");
        Stream<Integer> stream2 = Stream.of(1, 2, 3, 4, 5);
        
        // 传入数组
        String[] array = {"x", "y", "z"};
        Stream<String> stream3 = Stream.of(array);
        
        // 空Stream
        Stream<String> emptyStream = Stream.empty();
        
        // 使用示例
        stream1.forEach(System.out::print); // 输出: abc
        System.out.println();
        System.out.println(emptyStream.count()); // 输出: 0
    }
}

2.4 使用Stream.generate和Stream.iterate创建无限流

java 复制代码
import java.util.stream.Stream;
import java.util.Random;
import java.util.List;
import java.util.stream.Collectors;

public class CreateInfiniteStream {
    public static void main(String[] args) {
        // Stream.generate - 接受Supplier,生成无限流
        Stream<Double> randomStream = Stream.generate(Math::random);
        
        // 取前5个随机数
        List<Double> randomNumbers = randomStream
            .limit(5)
            .collect(Collectors.toList());
        System.out.println(randomNumbers);
        
        // 使用Random生成随机数
        Random random = new Random();
        Stream<Integer> randomIntStream = Stream.generate(random::nextInt);
        List<Integer> randomInts = randomIntStream
            .limit(5)
            .collect(Collectors.toList());
        System.out.println(randomInts);
        
        // Stream.iterate - 接受种子和UnaryOperator,生成无限流
        // 生成偶数序列: 0, 2, 4, 6, 8, ...
        Stream<Integer> evenNumbers = Stream.iterate(0, n -> n + 2);
        List<Integer> first5Evens = evenNumbers
            .limit(5)
            .collect(Collectors.toList());
        System.out.println(first5Evens); // 输出: [0, 2, 4, 6, 8]
        
        // JDK9+ 支持带条件的iterate
        // Stream.iterate(0, n -> n < 100, n -> n + 2) // n < 100时停止
    }
}

2.5 从I/O和其他源创建

java 复制代码
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
import java.io.BufferedReader;
import java.io.StringReader;

public class CreateFromIO {
    public static void main(String[] args) {
        // 从文件创建Stream(需要处理IOException)
        try {
            Stream<String> lines = Files.lines(Paths.get("test.txt"));
            // 处理文件内容
            // lines.forEach(System.out::println);
            lines.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 从BufferedReader创建Stream
        String content = "Line 1\nLine 2\nLine 3";
        BufferedReader reader = new BufferedReader(new StringReader(content));
        try (Stream<String> lineStream = reader.lines()) {
            lineStream.forEach(System.out::println);
        }
        // 输出:
        // Line 1
        // Line 2
        // Line 3
        
        // 从字符串创建字符Stream
        String str = "Hello";
        str.chars()  // 返回IntStream
           .mapToObj(c -> (char) c)
           .forEach(System.out::print);
        // 输出: Hello
    }
}

2.6 创建方式对比表

创建方式 方法 适用场景 示例
集合 collection.stream() 从List/Set等创建 list.stream()
数组(对象) Arrays.stream() 从对象数组创建 Arrays.stream(arr)
数组(基本类型) Arrays.stream() 从基本类型数组创建 Arrays.stream(intArr)
直接创建 Stream.of() 直接传入元素 Stream.of(1,2,3)
无限流 Stream.generate() 生成无限随机/常量流 Stream.generate(Math::random)
无限流 Stream.iterate() 生成有规律无限流 Stream.iterate(0, n->n+1)
文件 Files.lines() 从文件读取行 Files.lines(path)
字符串 String.chars() 字符流 "abc".chars()

三、Stream的中间操作

中间操作(Intermediate Operations)返回一个新的Stream,可以链式调用。它们是惰性求值的,不会立即执行。

3.1 filter - 过滤

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class FilterOperation {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        // 筛选偶数
        List<Integer> evens = numbers.stream()
            .filter(n -> n % 2 == 0)
            .collect(Collectors.toList());
        System.out.println(evens); // 输出: [2, 4, 6, 8, 10]
        
        // 筛选大于5的数
        List<Integer> greaterThan5 = numbers.stream()
            .filter(n -> n > 5)
            .collect(Collectors.toList());
        System.out.println(greaterThan5); // 输出: [6, 7, 8, 9, 10]
        
        // 多条件过滤
        List<Integer> complexFilter = numbers.stream()
            .filter(n -> n > 3 && n < 8)
            .filter(n -> n % 2 == 0)
            .collect(Collectors.toList());
        System.out.println(complexFilter); // 输出: [4, 6]
        
        // 对象过滤
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "");
        List<String> validNames = names.stream()
            .filter(s -> s != null && !s.isEmpty())
            .filter(s -> s.length() > 3)
            .collect(Collectors.toList());
        System.out.println(validNames); // 输出: [Alice, Charlie, David]
    }
}

3.2 map - 映射

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MapOperation {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("alice", "bob", "charlie");
        
        // 字符串转大写
        List<String> upperNames = names.stream()
            .map(String::toUpperCase)
            .collect(Collectors.toList());
        System.out.println(upperNames); // 输出: [ALICE, BOB, CHARLIE]
        
        // 获取字符串长度
        List<Integer> nameLengths = names.stream()
            .map(String::length)
            .collect(Collectors.toList());
        System.out.println(nameLengths); // 输出: [5, 3, 7]
        
        // 数字计算
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        List<Integer> squares = numbers.stream()
            .map(n -> n * n)
            .collect(Collectors.toList());
        System.out.println(squares); // 输出: [1, 4, 9, 16, 25]
        
        // 对象转换
        List<Person> people = Arrays.asList(
            new Person("Alice", 25),
            new Person("Bob", 30),
            new Person("Charlie", 35)
        );
        List<String> personNames = people.stream()
            .map(Person::getName)
            .collect(Collectors.toList());
        System.out.println(personNames); // 输出: [Alice, Bob, Charlie]
    }
}

class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() { return name; }
    public int getAge() { return age; }
}

3.3 flatMap - 扁平化映射

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class FlatMapOperation {
    public static void main(String[] args) {
        // 场景1:扁平化嵌套列表
        List<List<Integer>> nestedList = Arrays.asList(
            Arrays.asList(1, 2, 3),
            Arrays.asList(4, 5, 6),
            Arrays.asList(7, 8, 9)
        );
        
        // 使用map得到的是Stream<List<Integer>>
        // 使用flatMap得到的是Stream<Integer>
        List<Integer> flatList = nestedList.stream()
            .flatMap(List::stream)
            .collect(Collectors.toList());
        System.out.println(flatList); // 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]
        
        // 场景2:处理多个单词
        List<String> sentences = Arrays.asList(
            "Hello World",
            "Java Stream API",
            "Functional Programming"
        );
        List<String> words = sentences.stream()
            .flatMap(s -> Arrays.stream(s.split(" ")))
            .collect(Collectors.toList());
        System.out.println(words); // 输出: [Hello, World, Java, Stream, API, Functional, Programming]
        
        // 场景3:对象中的集合属性
        List<Department> departments = Arrays.asList(
            new Department("IT", Arrays.asList("Alice", "Bob")),
            new Department("HR", Arrays.asList("Charlie", "David")),
            new Department("Finance", Arrays.asList("Eve"))
        );
        
        List<String> allEmployees = departments.stream()
            .flatMap(d -> d.getEmployees().stream())
            .collect(Collectors.toList());
        System.out.println(allEmployees); // 输出: [Alice, Bob, Charlie, David, Eve]
    }
}

class Department {
    private String name;
    private List<String> employees;
    
    public Department(String name, List<String> employees) {
        this.name = name;
        this.employees = employees;
    }
    
    public List<String> getEmployees() { return employees; }
}

3.4 distinct - 去重

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class DistinctOperation {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 4, 4, 4);
        
        // 基本类型去重
        List<Integer> distinctNumbers = numbers.stream()
            .distinct()
            .collect(Collectors.toList());
        System.out.println(distinctNumbers); // 输出: [1, 2, 3, 4]
        
        // 字符串去重
        List<String> words = Arrays.asList("apple", "banana", "apple", "cherry", "banana");
        List<String> distinctWords = words.stream()
            .distinct()
            .collect(Collectors.toList());
        System.out.println(distinctWords); // 输出: [apple, banana, cherry]
        
        // 对象去重(基于equals和hashCode)
        List<Person> people = Arrays.asList(
            new Person("Alice", 25),
            new Person("Bob", 30),
            new Person("Alice", 25)  // 与第一个相同
        );
        List<Person> distinctPeople = people.stream()
            .distinct()
            .collect(Collectors.toList());
        System.out.println(distinctPeople.size()); // 输出取决于Person的equals实现
    }
}

3.5 sorted - 排序

java 复制代码
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class SortedOperation {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9, 3);
        
        // 自然排序(升序)
        List<Integer> sorted = numbers.stream()
            .sorted()
            .collect(Collectors.toList());
        System.out.println(sorted); // 输出: [1, 2, 3, 5, 8, 9]
        
        // 降序排序
        List<Integer> reverseSorted = numbers.stream()
            .sorted(Comparator.reverseOrder())
            .collect(Collectors.toList());
        System.out.println(reverseSorted); // 输出: [9, 8, 5, 3, 2, 1]
        
        // 自定义排序
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
        List<String> byLength = names.stream()
            .sorted(Comparator.comparing(String::length))
            .collect(Collectors.toList());
        System.out.println(byLength); // 输出: [Bob, Alice, David, Charlie]
        
        // 多级排序
        List<Person> people = Arrays.asList(
            new Person("Alice", 30),
            new Person("Bob", 25),
            new Person("Charlie", 30),
            new Person("David", 25)
        );
        List<Person> multiSorted = people.stream()
            .sorted(Comparator.comparing(Person::getAge)
                .thenComparing(Person::getName))
            .collect(Collectors.toList());
        multiSorted.forEach(p -> System.out.println(p.getName() + ": " + p.getAge()));
        // 输出:
        // Bob: 25
        // David: 25
        // Alice: 30
        // Charlie: 30
    }
}

class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() { return name; }
    public int getAge() { return age; }
}

3.6 peek - 查看元素

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class PeekOperation {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        
        // peek用于调试,查看流中的元素
        List<Integer> result = numbers.stream()
            .peek(n -> System.out.println("Original: " + n))
            .map(n -> n * n)
            .peek(n -> System.out.println("Squared: " + n))
            .filter(n -> n > 10)
            .peek(n -> System.out.println("Filtered: " + n))
            .collect(Collectors.toList());
        
        System.out.println("Result: " + result);
        // 输出:
        // Original: 1
        // Squared: 1
        // Original: 2
        // Squared: 4
        // Original: 3
        // Squared: 9
        // Original: 4
        // Squared: 16
        // Filtered: 16
        // Original: 5
        // Squared: 25
        // Filtered: 25
        // Result: [16, 25]
        
        // 注意:peek主要用于调试,不要在peek中修改元素状态
    }
}

3.7 limit和skip - 截取

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class LimitSkipOperation {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        // limit - 取前n个
        List<Integer> first3 = numbers.stream()
            .limit(3)
            .collect(Collectors.toList());
        System.out.println(first3); // 输出: [1, 2, 3]
        
        // skip - 跳过前n个
        List<Integer> skip3 = numbers.stream()
            .skip(3)
            .collect(Collectors.toList());
        System.out.println(skip3); // 输出: [4, 5, 6, 7, 8, 9, 10]
        
        // limit + skip 实现分页
        int pageSize = 3;
        int pageNumber = 2; // 第2页(从1开始)
        List<Integer> page = numbers.stream()
            .skip((pageNumber - 1) * pageSize)
            .limit(pageSize)
            .collect(Collectors.toList());
        System.out.println("Page " + pageNumber + ": " + page); // 输出: Page 2: [4, 5, 6]
        
        // 从无限流中取有限个
        List<Integer> infiniteSample = Stream.iterate(1, n -> n + 1)
            .limit(5)
            .collect(Collectors.toList());
        System.out.println(infiniteSample); // 输出: [1, 2, 3, 4, 5]
    }
}

3.8 中间操作对比表

操作 功能 返回值 示例
filter 过滤元素 Stream filter(n -> n > 5)
map 转换元素 Stream map(String::toUpperCase)
flatMap 扁平化 Stream flatMap(List::stream)
distinct 去重 Stream distinct()
sorted 排序 Stream sorted() / sorted(Comparator)
peek 查看元素 Stream peek(System.out::println)
limit 限制数量 Stream limit(10)
skip 跳过元素 Stream skip(5)

四、Stream的终止操作

终止操作(Terminal Operations)会触发实际计算并产生结果。执行终止操作后,Stream就被消费了,不能再次使用。

4.1 forEach - 遍历

java 复制代码
import java.util.Arrays;
import java.util.List;

public class ForEachOperation {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        
        // 基本遍历
        names.stream().forEach(System.out::println);
        // 输出:
        // Alice
        // Bob
        // Charlie
        
        // 带索引的遍历(需要借助IntStream)
        List<String> items = Arrays.asList("a", "b", "c");
        int[] index = {0};
        items.forEach(item -> {
            System.out.println(index[0] + ": " + item);
            index[0]++;
        });
        // 输出:
        // 0: a
        // 1: b
        // 2: c
        
        // 注意:forEach是终止操作,之后不能再链式调用
    }
}

4.2 collect - 收集

java 复制代码
import java.util.*;
import java.util.stream.Collectors;

public class CollectOperation {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
        
        // 收集到List
        List<String> list = names.stream()
            .collect(Collectors.toList());
        
        // 收集到Set
        Set<String> set = names.stream()
            .collect(Collectors.toSet());
        
        // 收集到指定集合类型
        LinkedList<String> linkedList = names.stream()
            .collect(Collectors.toCollection(LinkedList::new));
        
        // 收集到Map
        Map<String, Integer> nameLengthMap = names.stream()
            .collect(Collectors.toMap(
                name -> name,
                String::length
            ));
        System.out.println(nameLengthMap); // 输出: {Bob=3, Alice=5, David=5, Charlie=7}
        
        // 处理重复key的情况
        List<String> withDuplicates = Arrays.asList("a", "b", "a");
        Map<String, Integer> mapWithMerge = withDuplicates.stream()
            .collect(Collectors.toMap(
                s -> s,
                s -> 1,
                (existing, replacement) -> existing + replacement
            ));
        System.out.println(mapWithMerge); // 输出: {a=2, b=1}
        
        // 字符串连接
        String joined = names.stream()
            .collect(Collectors.joining(", "));
        System.out.println(joined); // 输出: Alice, Bob, Charlie, David
        
        // 带前缀后缀的连接
        String formatted = names.stream()
            .collect(Collectors.joining(", ", "[", "]"));
        System.out.println(formatted); // 输出: [Alice, Bob, Charlie, David]
        
        // 分组
        Map<Integer, List<String>> groupedByLength = names.stream()
            .collect(Collectors.groupingBy(String::length));
        System.out.println(groupedByLength); // 输出: {3=[Bob], 5=[Alice, David], 7=[Charlie]}
        
        // 分区(特殊的分组,分为true和false两组)
        Map<Boolean, List<String>> partitioned = names.stream()
            .collect(Collectors.partitioningBy(s -> s.length() > 4));
        System.out.println(partitioned); // 输出: {false=[Bob], true=[Alice, Charlie, David]}
        
        // 统计
        IntSummaryStatistics stats = names.stream()
            .collect(Collectors.summarizingInt(String::length));
        System.out.println("Count: " + stats.getCount());     // 输出: Count: 4
        System.out.println("Sum: " + stats.getSum());         // 输出: Sum: 20
        System.out.println("Min: " + stats.getMin());         // 输出: Min: 3
        System.out.println("Max: " + stats.getMax());         // 输出: Max: 7
        System.out.println("Average: " + stats.getAverage()); // 输出: Average: 5.0
    }
}

4.3 toArray - 转换为数组

java 复制代码
import java.util.Arrays;
import java.util.List;

public class ToArrayOperation {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        
        // 转换为Object数组
        Object[] objectArray = names.stream().toArray();
        System.out.println(Arrays.toString(objectArray)); // 输出: [Alice, Bob, Charlie]
        
        // 转换为指定类型数组
        String[] stringArray = names.stream()
            .toArray(String[]::new);
        System.out.println(Arrays.toString(stringArray)); // 输出: [Alice, Bob, Charlie]
        
        // 基本类型数组(使用IntStream等特化流)
        int[] intArray = names.stream()
            .mapToInt(String::length)
            .toArray();
        System.out.println(Arrays.toString(intArray)); // 输出: [5, 3, 7]
    }
}

4.4 reduce - 归约

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class ReduceOperation {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        
        // 求和(带初始值)
        int sum = numbers.stream()
            .reduce(0, (a, b) -> a + b);
        System.out.println("Sum: " + sum); // 输出: Sum: 15
        
        // 求和(方法引用)
        int sum2 = numbers.stream()
            .reduce(0, Integer::sum);
        System.out.println("Sum2: " + sum2); // 输出: Sum2: 15
        
        // 求和(不带初始值,返回Optional)
        Optional<Integer> sumOptional = numbers.stream()
            .reduce((a, b) -> a + b);
        sumOptional.ifPresent(s -> System.out.println("Sum Optional: " + s));
        
        // 乘法
        int product = numbers.stream()
            .reduce(1, (a, b) -> a * b);
        System.out.println("Product: " + product); // 输出: Product: 120
        
        // 字符串连接
        List<String> words = Arrays.asList("Hello", " ", "World", "!");
        String sentence = words.stream()
            .reduce("", String::concat);
        System.out.println(sentence); // 输出: Hello World!
        
        // 找最大值
        Optional<Integer> max = numbers.stream()
            .reduce(Integer::max);
        max.ifPresent(m -> System.out.println("Max: " + m)); // 输出: Max: 5
        
        // 找最小值
        Optional<Integer> min = numbers.stream()
            .reduce(Integer::min);
        min.ifPresent(m -> System.out.println("Min: " + m)); // 输出: Min: 1
        
        // 复杂归约:拼接带分隔符的字符串
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        String joined = names.stream()
            .reduce((a, b) -> a + ", " + b)
            .orElse("");
        System.out.println(joined); // 输出: Alice, Bob, Charlie
    }
}

4.5 min/max/count - 统计

java 复制代码
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

public class MinMaxCountOperation {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9, 3);
        
        // count - 计数
        long count = numbers.stream().count();
        System.out.println("Count: " + count); // 输出: Count: 6
        
        // max - 最大值
        Optional<Integer> max = numbers.stream()
            .max(Integer::compareTo);
        max.ifPresent(m -> System.out.println("Max: " + m)); // 输出: Max: 9
        
        // min - 最小值
        Optional<Integer> min = numbers.stream()
            .min(Integer::compareTo);
        min.ifPresent(m -> System.out.println("Min: " + m)); // 输出: Min: 1
        
        // 使用自定义比较器
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
        
        // 按长度找最长
        Optional<String> longest = names.stream()
            .max(Comparator.comparing(String::length));
        longest.ifPresent(s -> System.out.println("Longest: " + s)); // 输出: Longest: Charlie
        
        // 按长度找最短
        Optional<String> shortest = names.stream()
            .min(Comparator.comparing(String::length));
        shortest.ifPresent(s -> System.out.println("Shortest: " + s)); // 输出: Shortest: Bob
        
        // 按字母顺序
        Optional<String> firstAlphabetically = names.stream()
            .min(String::compareTo);
        firstAlphabetically.ifPresent(s -> System.out.println("First: " + s)); // 输出: First: Alice
    }
}

4.6 匹配操作

java 复制代码
import java.util.Arrays;
import java.util.List;

public class MatchOperation {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(2, 4, 6, 8, 10);
        
        // anyMatch - 是否有任意一个匹配
        boolean hasEven = numbers.stream()
            .anyMatch(n -> n % 2 == 0);
        System.out.println("Has even: " + hasEven); // 输出: Has even: true
        
        boolean hasGreaterThan5 = numbers.stream()
            .anyMatch(n -> n > 5);
        System.out.println("Has > 5: " + hasGreaterThan5); // 输出: Has > 5: true
        
        // allMatch - 是否全部匹配
        boolean allEven = numbers.stream()
            .allMatch(n -> n % 2 == 0);
        System.out.println("All even: " + allEven); // 输出: All even: true
        
        boolean allGreaterThan5 = numbers.stream()
            .allMatch(n -> n > 5);
        System.out.println("All > 5: " + allGreaterThan5); // 输出: All > 5: false
        
        // noneMatch - 是否全部不匹配
        boolean noOdd = numbers.stream()
            .noneMatch(n -> n % 2 != 0);
        System.out.println("No odd: " + noOdd); // 输出: No odd: true
        
        // 空流的匹配
        List<Integer> empty = Arrays.asList();
        System.out.println("Empty anyMatch: " + empty.stream().anyMatch(n -> n > 0)); // false
        System.out.println("Empty allMatch: " + empty.stream().allMatch(n -> n > 0)); // true
        System.out.println("Empty noneMatch: " + empty.stream().noneMatch(n -> n > 0)); // true
    }
}

4.7 findFirst/findAny - 查找

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class FindOperation {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
        
        // findFirst - 返回第一个元素(串行流中确定性)
        Optional<String> first = names.stream()
            .findFirst();
        first.ifPresent(s -> System.out.println("First: " + s)); // 输出: First: Alice
        
        // findAny - 返回任意一个元素(并行流中更高效)
        Optional<String> any = names.stream()
            .findAny();
        any.ifPresent(s -> System.out.println("Any: " + s)); // 输出: Any: Alice(串行时通常是第一个)
        
        // 结合filter使用
        Optional<String> firstWithC = names.stream()
            .filter(s -> s.startsWith("C"))
            .findFirst();
        firstWithC.ifPresent(s -> System.out.println("First with C: " + s)); // 输出: First with C: Charlie
        
        // 找不到时返回empty Optional
        Optional<String> firstWithZ = names.stream()
            .filter(s -> s.startsWith("Z"))
            .findFirst();
        System.out.println("First with Z present: " + firstWithZ.isPresent()); // 输出: false
    }
}

4.8 终止操作对比表

操作 功能 返回值 示例
forEach 遍历元素 void forEach(System.out::println)
collect 收集到集合 集合 collect(Collectors.toList())
toArray 转换为数组 数组 toArray(String[]::new)
reduce 归约 Optional/值 reduce(0, Integer::sum)
count 计数 long count()
min 最小值 Optional min(Comparator)
max 最大值 Optional max(Comparator)
anyMatch 任意匹配 boolean anyMatch(Predicate)
allMatch 全部匹配 boolean allMatch(Predicate)
noneMatch 全部不匹配 boolean noneMatch(Predicate)
findFirst 找第一个 Optional findFirst()
findAny 找任意一个 Optional findAny()

五、经验之谈:Stream操作是惰性求值的

5.1 什么是惰性求值

Stream的中间操作是**惰性(Lazy)**的,它们不会立即执行,而是等到终止操作被调用时才会一并执行。这种设计带来了性能优化:

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class LazyEvaluation {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        System.out.println("开始创建Stream...");
        
        var stream = numbers.stream()
            .peek(n -> System.out.println("peek: " + n))  // 中间操作
            .filter(n -> {
                System.out.println("filter: " + n);
                return n % 2 == 0;
            })
            .map(n -> {
                System.out.println("map: " + n);
                return n * n;
            });
        
        System.out.println("Stream创建完成,尚未执行任何操作");
        System.out.println("---");
        
        // 直到调用终止操作,中间操作才会执行
        System.out.println("调用终止操作...");
        List<Integer> result = stream.limit(3).collect(Collectors.toList());
        System.out.println("结果: " + result);
        
        // 注意:由于limit(3)的存在,Stream在处理完3个元素后就停止了
        // 这就是短路操作带来的优化
    }
}

5.2 短路操作的优势

某些中间操作是**短路(Short-circuiting)**的,它们可以在满足条件时提前终止处理:

java 复制代码
import java.util.stream.Stream;
import java.util.List;
import java.util.stream.Collectors;

public class ShortCircuiting {
    public static void main(String[] args) {
        // 无限流 + limit = 安全的操作
        List<Integer> first10 = Stream.iterate(1, n -> n + 1)
            .peek(n -> System.out.println("生成: " + n))
            .limit(10)
            .collect(Collectors.toList());
        System.out.println("前10个: " + first10);
        // 只生成了10个元素,不会无限循环
        
        // findFirst也是短路操作
        Integer firstEven = Stream.iterate(1, n -> n + 1)
            .peek(n -> System.out.println("检查: " + n))
            .filter(n -> n % 2 == 0)
            .findFirst()
            .orElse(0);
        System.out.println("第一个偶数: " + firstEven);
        // 只检查了1和2就返回了
    }
}

5.3 短路操作列表

操作类型 操作 说明
中间操作 limit 限制元素数量
中间操作 skip 跳过元素
终止操作 findFirst 找到第一个就停止
终止操作 findAny 找到任意一个就停止
终止操作 anyMatch 有一个匹配就停止
终止操作 allMatch 有一个不匹配就停止
终止操作 noneMatch 有一个匹配就停止

六、踩坑提醒

6.1 Stream只能消费一次

坑点:Stream被终止操作消费后,不能再次使用。

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamConsumedPitfall {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        
        java.util.stream.Stream<String> stream = names.stream();
        
        // 第一次使用
        List<String> list1 = stream.collect(Collectors.toList());
        System.out.println(list1);
        
        // 错误:Stream已经被消费,再次使用会抛异常
        // List<String> list2 = stream.collect(Collectors.toList());
        // 抛出: java.lang.IllegalStateException: stream has already been operated upon or closed
        
        // 正确做法:重新创建Stream
        List<String> list2 = names.stream().collect(Collectors.toList());
        System.out.println(list2);
    }
}

6.2 不要修改源数据

坑点:Stream操作不应修改源数据,这会导致不可预期的结果,特别是在并行流中。

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ModifySourcePitfall {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(Arrays.asList("Alice", "Bob", "Charlie"));
        
        // 错误:在Stream操作中修改源数据
        // names.stream().forEach(names::remove); // 会抛出ConcurrentModificationException
        
        // 错误:使用filter后修改原集合
        List<String> filtered = names.stream()
            .filter(s -> s.length() > 3)
            .collect(java.util.stream.Collectors.toList());
        names.clear(); // 这会影响到后续可能使用的filtered吗?不会,但容易混淆
        
        // 正确做法:Stream操作不修改源数据,只产生新数据
        List<String> upperNames = names.stream()
            .map(String::toUpperCase)  // 创建新字符串,不修改原字符串
            .collect(java.util.stream.Collectors.toList());
        
        System.out.println("原始: " + names);       // 输出: 原始: []
        System.out.println("转换后: " + upperNames); // 输出: 转换后: []
    }
}

6.3 注意空指针异常

java 复制代码
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class NullPointerPitfall {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", null, "Bob", null, "Charlie");
        
        // 错误:直接操作可能包含null的Stream
        // List<Integer> lengths = names.stream()
        //     .map(String::length)  // 会抛出NullPointerException
        //     .collect(Collectors.toList());
        
        // 正确做法:先过滤null
        List<Integer> lengths = names.stream()
            .filter(s -> s != null)
            .map(String::length)
            .collect(Collectors.toList());
        System.out.println(lengths); // 输出: [5, 3, 7]
        
        // 或者使用Objects.nonNull
        List<String> nonNullNames = names.stream()
            .filter(java.util.Objects::nonNull)
            .collect(Collectors.toList());
        System.out.println(nonNullNames); // 输出: [Alice, Bob, Charlie]
    }
}

6.4 并行流的线程安全问题

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

public class ParallelStreamPitfall {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        // 错误:在并行流中使用非线程安全的集合
        List<Integer> result = new ArrayList<>();
        numbers.parallelStream()
            .forEach(result::add); // 可能丢数据或抛异常
        System.out.println("结果数量: " + result.size()); // 可能少于10
        
        // 正确做法1:使用collect合并结果
        List<Integer> result2 = numbers.parallelStream()
            .collect(Collectors.toList());
        System.out.println("结果2数量: " + result2.size()); // 输出: 10
        
        // 正确做法2:使用线程安全的集合(性能较差)
        List<Integer> result3 = new CopyOnWriteArrayList<>();
        numbers.parallelStream()
            .forEach(result3::add);
        System.out.println("结果3数量: " + result3.size()); // 输出: 10
    }
}

七、面试高频考点

7.1 Stream和Collection的区别?

特性 Collection Stream
存储 存储数据元素 不存储数据,是计算视图
遍历 外部迭代(for/iterator) 内部迭代
使用次数 可重复使用 只能消费一次
修改 可以修改元素 不修改源数据
执行方式 立即执行 惰性求值
并行支持 需手动实现 一行代码切换
适用场景 数据存储 数据计算

7.2 Stream的惰性求值是什么意思?

**惰性求值(Lazy Evaluation)**是指Stream的中间操作不会立即执行,而是被记录下来,等到终止操作被调用时才一并执行。

好处

  1. 性能优化:可以合并多个操作,减少遍历次数
  2. 短路优化:可以在满足条件时提前终止,避免不必要的计算
  3. 支持无限流:可以处理理论上无限的数据源
java 复制代码
list.stream()
    .filter(...)  // 不执行
    .map(...)     // 不执行
    .sorted(...)  // 不执行
    .collect(...); // 此时所有操作一起执行

7.3 什么时候使用并行流?

适合使用并行流的情况

  1. 数据量大(通常 > 10,000 个元素)
  2. 计算复杂(每个元素的处理耗时较长)
  3. 无状态、无副作用的操作
  4. 数据源可高效分割(如ArrayList、数组、IntStream.range)

不适合使用并行流的情况

  1. 数据量小(线程切换开销 > 并行收益)
  2. 需要保证顺序(虽然可以ordered(),但有性能损耗)
  3. 涉及I/O操作(阻塞common pool)
  4. 需要修改共享变量
  5. 数据源分割效率低(如LinkedList、Stream.iterate)

八、总结

今天我们深入学习了JDK8的Stream API(上篇),主要内容包括:

  1. 为什么需要Stream API:解决了传统集合操作代码冗长、可读性差的问题
  2. Stream的创建方式:从集合、数组、Stream.of、generate/iterate等多种方式
  3. 中间操作:filter、map、flatMap、distinct、sorted、peek、limit、skip
  4. 终止操作:forEach、collect、toArray、reduce、min/max/count、match、find
  5. 惰性求值:中间操作延迟执行,带来性能优化
  6. 常见坑点:Stream只能消费一次、不要修改源数据、注意线程安全

下一步预告

在Day5中,我们将继续学习Stream API(下),内容包括:

  • 数值特化流(IntStream、LongStream、DoubleStream)
  • 高级Collectors用法
  • 并行流的原理与最佳实践
  • Stream的性能优化技巧

敬请期待!


参考资料

  1. Java 8 Stream API官方文档
  2. Java 8 Streams Tutorial
  3. Stream API最佳实践

互动话题

  1. 你在项目中使用Stream API了吗?最常用哪些操作?
  2. 有没有因为Stream的惰性求值特性而踩过坑?
  3. 你觉得Stream API让代码更易读还是更难理解了?

欢迎在评论区分享你的经验和看法,点赞收藏不迷路,我们Day5见!

相关推荐
超梦dasgg12 小时前
拆分大对象 + 流式处理 + 不一次性加载全量数据
java·jvm·windows
A南方故人12 小时前
将容器内的元素变为可拖拽
开发语言·javascript·ecmascript
小小de风呀12 小时前
de风——【从零开始学C++】(九)—vector的基本使用
开发语言·c++
MepSUxjvy12 小时前
002:RAG 入门-LangChain 读取文本
开发语言·python·langchain
我是一颗柠檬12 小时前
【JDK8新特性】方法引用与构造器引用Day3
java·开发语言·后端·intellij-idea
在繁华处12 小时前
从零搭建轻灵(五):记忆系统与生产化特性
java·jvm·oracle
子榆.12 小时前
CANN自定义GEMM算子(Ascend C手写高性能矩阵乘法)
c语言·开发语言·矩阵
天若有情67312 小时前
Deepseek-V4-Flash-20260423 深度评测与实战指南
java·大数据·网络·ai
折哥的程序人生 · 物流技术专研12 小时前
《Java 100 天进阶之路》第32篇:Java常用工具类(Objects、Collections、Arrays深入)
java·后端·面试·求职招聘