15.Java 8 Stream 流入门到实战

Java 8、Stream、函数式编程、Lambda、集合处理

一、什么是 Stream 流

Stream 是 Java 8 引入的关键抽象,它不是数据结构,不存储数据,而是用于对集合(Collection)、数组等数据源进行函数式、声明式的批量数据处理。

简单理解:Stream 就像一条"流水线",数据从源头流入,经过一道道工序(中间操作),最终产出结果(终端操作)。

核心思想

  • 流水线思想:数据从源头经过一系列中间操作得到结果,每个操作不修改原始数据源,而是产生新的 Stream。
  • 函数式编程:使用 Lambda 表达式和方法引用,代码更简洁、可读性更强。
  • 惰性求值:中间操作(filter、map 等)不会立即执行,只有遇到终端操作(collect、forEach)时才触发计算。
  • 内部迭代:由 Stream API 内部完成迭代,开发者只需关注"做什么",而非"怎么做"。

二、Stream 的核心特点

特点 说明
不存储数据 Stream 本身不保存元素,只是对数据源的视图/管道
函数式编程 对 Stream 的操作不会修改原始数据源
惰性求值 中间操作不会立即执行,只有终端操作触发时才真正计算
可消费性 一个 Stream 只能被消费一次,用完即关闭
并行能力 可以轻松切换为并行流 parallelStream(),充分利用多核 CPU

三、与传统集合处理的对比

对比维度 传统方式 Stream 流
迭代方式 外部迭代(手动 for/while) 内部迭代(API 自动处理)
代码风格 命令式(告诉怎么做) 声明式(告诉做什么)
并行处理 需手动编写多线程代码 parallelStream() 一键并行
链式调用 不支持 支持 .filter().map().collect()
数据源修改 可能修改原数据 不修改原数据源

四、Stream 操作的三大阶段

复制代码
数据源  →  中间操作  →  终端操作
(集合/数组)  (filter/map/sorted...)  (collect/forEach/count...)

4.1 获取 Stream 的 9 种方式

复制代码
// 1. 从单列集合获取
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream1 = list.stream();            // 顺序流
Stream<String> stream2 = list.parallelStream();    // 并行流

// 2. 从 Map 集合获取(间接)
Map<String, Integer> map = new HashMap<>();
Stream<String> keyStream = map.keySet().stream();          // 键流
Stream<Integer> valueStream = map.values().stream();       // 值流
Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream(); // 键值对流

// 3. 从数组获取
String[] arr = {"x", "y", "z"};
Stream<String> stream3 = Arrays.stream(arr);

// 4. 直接创建零散数据
Stream<String> stream4 = Stream.of("Java", "Python", "C++");
Stream<Integer> stream5 = Stream.of(1, 2, 3, 4, 5);

// 5. 使用 Stream.builder()
Stream<String> stream6 = Stream.<String>builder()
        .add("Hello")
        .add("World")
        .build();

// 6. 生成无限流(必须用 limit 截断)
Stream<Integer> iterStream = Stream.iterate(0, n -> n + 2).limit(10); // 0,2,4...
Stream<Double> generateStream = Stream.generate(Math::random).limit(5);

// 7. 基本类型流(避免装箱开销)
IntStream intStream = IntStream.of(1, 2, 3);
LongStream longStream = LongStream.range(0, 100);          // [0, 100)
LongStream rangeClosed = LongStream.rangeClosed(0, 100);   // [0, 100]
DoubleStream doubleStream = DoubleStream.of(1.0, 2.5, 3.8);

// 8. 从文件获取
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
    lines.forEach(System.out::println);
}

// 9. 从字符串拆分获取
String str = "apple,banana,orange";
Stream<String> stream9 = Pattern.compile(",").splitAsStream(str);

五、中间操作(Intermediate Operations)

中间操作返回一个新的 Stream,可以链式调用,惰性执行

5.1 常用中间方法一览

方法 作用 示例
filter(Predicate) 按条件过滤 .filter(x -> x > 10)
map(Function) 转换每个元素 .map(String::toUpperCase)
flatMap(Function) 将每个元素展开为多个元素 .flatMap(s -> Arrays.stream(s.split(",")))
distinct() 去重(依赖 equals/hashCode) .distinct()
sorted() 自然排序 .sorted()
sorted(Comparator) 自定义排序 .sorted(Comparator.reverseOrder())
limit(long n) 截取前 n 个 .limit(5)
skip(long n) 跳过前 n 个 .skip(3)
peek(Consumer) 调试/查看每个元素 .peek(System.out::println)
takeWhile(Predicate) 从头取,直到条件为假(Java 9+) .takeWhile(x -> x < 10)
dropWhile(Predicate) 从头丢弃,直到条件为假(Java 9+) .dropWhile(x -> x < 10)

5.2 方法详解

filter ------ 过滤:保留满足条件的元素。

复制代码
List<String> list = List.of("张三", "张无忌", "赵敏", "周芷若");
list.stream()
    .filter(name -> name.startsWith("张"))   // 只保留姓张的
    .forEach(System.out::println);
// 输出:张三  张无忌

map ------ 转换:把流中每个元素映射成另一种形式(一对一)。

复制代码
List<String> list = List.of("apple", "banana", "cherry");
list.stream()
    .map(String::toUpperCase)              // 全部转大写
    .forEach(System.out::println);
// 输出:APPLE  BANANA  CHERRY

// 提取对象中的字段
students.stream()
    .map(Student::getName)                  // Student → String
    .forEach(System.out::println);

flatMap ------ 扁平映射:每个元素映射成多个元素,再"拍平"成一个流(一对多)。

复制代码
// 把多个单词拆成单个字符
List<String> words = List.of("hello", "world");
words.stream()
    .flatMap(word -> Arrays.stream(word.split("")))
    .distinct()
    .forEach(System.out::print);
// 输出:helowrd

// 嵌套列表扁平化
List<List<Integer>> nested = Arrays.asList(
        Arrays.asList(1, 2),
        Arrays.asList(3, 4, 5),
        Arrays.asList(6)
);
List<Integer> flat = nested.stream()
        .flatMap(List::stream)
        .collect(Collectors.toList());
// 结果:[1, 2, 3, 4, 5, 6]

一句话区分:map 是一对一,flatMap 是一对多

distinct ------ 去重 :底层依赖 equals()hashCode(),自定义对象需重写这两个方法。

复制代码
List<Integer> list = List.of(1, 2, 2, 3, 3, 3, 4);
list.stream().distinct().forEach(System.out::print);
// 输出:1234

sorted ------ 排序

复制代码
// 自然排序(元素必须实现 Comparable)
Stream.of(3, 1, 5, 2, 4).sorted().forEach(System.out::print);
// 输出:12345

// 自定义比较器(按字符串长度升序)
Stream.of("aaa", "b", "cc")
      .sorted((a, b) -> a.length() - b.length())
      .forEach(System.out::println);
// 输出:b  cc  aaa

// 方法引用降序
Stream.of(3, 1, 5).sorted(Comparator.reverseOrder());

limit & skip ------ 截取与跳过(常用于分页)

复制代码
Stream.of(1, 2, 3, 4, 5).limit(3).forEach(System.out::print);  // 123
Stream.of(1, 2, 3, 4, 5).skip(2).forEach(System.out::print);   // 345

// 第 2 页,每页 2 条 → 跳过前 2 条,取 2 条
Stream.of(1, 2, 3, 4, 5).skip(2).limit(2).forEach(System.out::print);
// 输出:34

peek ------ 调试:不改变流内容,常用于观察每一步中间结果。

复制代码
list.stream()
    .filter(s -> s.length() > 3)
    .peek(s -> System.out.println("过滤后:" + s))
    .map(String::toUpperCase)
    .peek(s -> System.out.println("映射后:" + s))
    .collect(Collectors.toList());

5.3 链式调用示例

复制代码
List<String> names = List.of("张三丰", "张无忌", "赵敏", "周芷若", "张三", "韦小宝");

List<String> result = names.stream()
        .filter(name -> name.startsWith("张"))      // 1. 过滤姓张的
        .skip(1)                                    // 2. 跳过第一个
        .limit(2)                                   // 3. 只取 2 个
        .sorted((a, b) -> b.length() - a.length())  // 4. 按名字长度降序
        .collect(Collectors.toList());

System.out.println(result);   // [张无忌, 张三]

六、终端操作(Terminal Operations)

终端操作触发流水线执行,执行后 Stream 被消费关闭。

6.1 终端方法总览

方法 返回类型 作用
forEach void 遍历每个元素(并行流不保证顺序)
forEachOrdered void 按遭遇顺序遍历(并行流也保证顺序)
collect R 将结果收集为集合/字符串等
toList List 收集为不可变 List(Java 16+)
toArray Object\[\] / A\[\] 转为数组
count long 统计元素个数
reduce Optional / T 聚合计算(求和、求积等)
min / max Optional 最大/最小值
anyMatch boolean 是否有任一元素匹配
allMatch boolean 是否全部元素匹配
noneMatch boolean 是否没有元素匹配
findFirst Optional 返回第一个元素
findAny Optional 返回任意一个元素(并行流更高效)
iterator Iterator 获取迭代器

6.2 关键方法详解

reduce ------ 归约(三种重载形式):

复制代码
// ① 单参数:返回 Optional
Optional<Integer> sum1 = Stream.of(1, 2, 3).reduce((a, b) -> a + b);

// ② 双参数(带初始值):返回具体值
int sum2 = Stream.of(1, 2, 3).reduce(0, (a, b) -> a + b);   // 6

// ③ 三参数(并行归约)
int result = Stream.of(1, 2, 3).parallel()
        .reduce(0, (a, b) -> a + b, (a, b) -> a + b);

匹配类方法对比

方法 匹配规则
anyMatch 至少一个匹配
allMatch 全部匹配
noneMatch 无一匹配
复制代码
boolean hasAdult = persons.stream().anyMatch(p -> p.getAge() >= 18);
boolean allAdult = persons.stream().allMatch(p -> p.getAge() >= 18);
boolean noMinor  = persons.stream().noneMatch(p -> p.getAge() < 18);

基础示例

复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 统计偶数个数
long count = numbers.stream().filter(n -> n % 2 == 0).count();   // 5

// 求和
int sum = numbers.stream().reduce(0, Integer::sum);              // 55

// 判断是否存在大于 5 的数
boolean hasBig = numbers.stream().anyMatch(n -> n > 5);           // true

七、Stream 的 10 种遍历方式

场景 推荐方式
简单打印 forEach
并行流需保证顺序 forEachOrdered
转换后遍历 map + forEach
调试查看中间结果 peek(中间操作,需配终端操作)
高性能批量 parallelStream() + forEach
复制代码
// 1. forEach 最常用
list.stream().forEach(System.out::println);

// 2. forEachOrdered 并行流保证顺序
list.parallelStream().forEachOrdered(System.out::println);

// 3. collect 收集后再遍历
List<String> newList = list.stream()
        .filter(name -> name.length() > 3)
        .collect(Collectors.toList());
newList.forEach(System.out::println);

// 4. reduce 累积
int sum = list.stream().reduce(0, Integer::sum);
String result = list.stream().reduce("", String::concat);

// 5. 转数组后遍历
String[] array = list.stream().toArray(String[]::new);
Arrays.stream(array).forEach(System.out::println);

// 6. iterator 迭代器
Iterator<String> it = list.stream().iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

// 7. Spliterator 高级遍历
Spliterator<String> spliterator = list.stream().spliterator();
spliterator.forEachRemaining(System.out::println);

// 8. map + forEach 转换后遍历
list.stream().map(String::toUpperCase).forEach(System.out::println);

// 9. peek 调试用(中间操作,必须配合终端操作才会执行)
list.stream()
    .peek(name -> System.out.println("过滤前: " + name))
    .filter(name -> name.length() > 3)
    .forEach(System.out::println);

// 10. 并行流遍历
list.parallelStream().forEach(System.out::println);          // 不保证顺序
list.parallelStream().forEachOrdered(System.out::println);  // 保证顺序

八、Collectors 收集器详解

collect() 是最灵活的终端操作,配合 Collectors 工具类使用。

8.1 收集为集合

复制代码
List<String> list = Arrays.asList("apple", "banana", "cherry", "date", "apple");

// 收集为 List
List<String> toList = list.stream().collect(Collectors.toList());

// 收集为 Set(自动去重)
Set<String> toSet = list.stream().collect(Collectors.toSet());

// 收集为 Map(注意重复 key 需指定合并策略)
Map<String, Integer> toMap = list.stream()
        .distinct()
        .collect(Collectors.toMap(
                Function.identity(),    // key: 元素本身
                String::length          // value: 元素长度
        ));

// toMap 遇重复 key 默认抛 IllegalStateException,必须传 mergeFunction
Map<Integer, String> map = Stream.of("Java", "Python", "Go")
        .collect(Collectors.toMap(
                String::length,
                s -> s,
                (oldVal, newVal) -> oldVal   // 冲突时保留旧值
        ));
// 结果:{2=Go, 4=Java, 6=Python}

8.2 joining 字符串拼接

复制代码
// 直接拼接
String s1 = Stream.of("A", "B", "C").collect(Collectors.joining());   // "ABC"

// 带分隔符
String s2 = Stream.of("Java", "Python", "Go")
        .collect(Collectors.joining(", "));   // "Java, Python, Go"

// 带前后缀
String s3 = Stream.of("Java", "Python", "Go")
        .collect(Collectors.joining(", ", "[", "]"));   // "[Java, Python, Go]"

8.3 统计

复制代码
// 计数
long count = Stream.of(1, 2, 3, 4, 5).collect(Collectors.counting());   // 5

// 求和
int sum = Stream.of(1, 2, 3, 4, 5)
        .collect(Collectors.summingInt(Integer::intValue));              // 15

// 平均值
double avg = Stream.of(10, 20, 30)
        .collect(Collectors.averagingInt(Integer::intValue));            // 20.0

// 一次性拿到统计信息
IntSummaryStatistics stats = list.stream()
        .collect(Collectors.summarizingInt(String::length));
stats.getCount(); stats.getSum(); stats.getMax(); stats.getMin(); stats.getAverage();

8.4 maxBy / minBy

复制代码
Optional<Integer> max = Stream.of(3, 7, 2, 9, 1)
        .collect(Collectors.maxBy(Integer::compareTo));     // Optional[9]

Optional<String> minStr = Stream.of("apple", "banana", "cherry")
        .collect(Collectors.minBy(Comparator.comparingInt(String::length)));
// Optional[apple](长度最短)

8.5 groupingBy 分组(重点)

类似 SQL 的 GROUP BY,按某个属性将元素分组。

复制代码
// 按字符串长度分组
Map<Integer, List<String>> groups = Stream.of("Java", "Go", "Rust", "C", "Python")
        .collect(Collectors.groupingBy(String::length));
// {1=[C], 2=[Go], 4=[Java, Rust], 6=[Python]}

// 分组 + 下游收集器:统计每组数量
Map<Integer, Long> countByLen = Stream.of("Java", "Go", "Rust", "C", "Python")
        .collect(Collectors.groupingBy(
                String::length,
                Collectors.counting()
        ));
// {1=1, 2=1, 4=2, 6=1}

groupingBy 三种重载

方法 说明
groupingBy(classifier) 基础分组,value 默认 List
groupingBy(classifier, downstream) 指定下游收集器(counting、toSet 等)
groupingBy(classifier, mapFactory, downstream) 指定返回的 Map 类型(如 TreeMap)

8.6 partitioningBy 分区

按布尔条件分为 true / false 两组,是 groupingBy 的特例。

复制代码
// 按是否为偶数分区
Map<Boolean, List<Integer>> partitions = Stream.of(1, 2, 3, 4, 5, 6)
        .collect(Collectors.partitioningBy(n -> n % 2 == 0));
// {false=[1, 3, 5], true=[2, 4, 6]}

// 分区 + 下游收集器
Map<Boolean, Long> countByPartition = Stream.of(1, 2, 3, 4, 5, 6)
        .collect(Collectors.partitioningBy(n -> n % 2 == 0, Collectors.counting()));
// {false=3, true=3}

8.7 collectingAndThen 收集后转换

先收集,再对结果做一次转换,常用于包装为不可变集合。

复制代码
// 收集为 List 后包装成不可变集合
List<String> unmodifiable = Stream.of("A", "B", "C")
        .collect(Collectors.collectingAndThen(
                Collectors.toList(),
                Collections::unmodifiableList
        ));
// unmodifiable 调用 add/remove 会抛 UnsupportedOperationException

8.8 Collectors 速查表

方法 用途 返回类型
toList() 收集到 List List<T>
toSet() 收集到 Set(去重) Set<T>
toMap(k, v) 收集到 Map Map<K, V>
toMap(k, v, merge) 收集到 Map(含冲突策略) Map<K, V>
joining() 字符串拼接 String
joining(delimiter) 带分隔符拼接 String
joining(delimiter, prefix, suffix) 带前后缀拼接 String
counting() 计数 Long
summingInt() 求和 Integer
averagingInt() 求均值 Double
maxBy(comparator) 取最大值 Optional<T>
minBy(comparator) 取最小值 Optional<T>
groupingBy(classifier) 分组 Map<K, List<T>>
groupingBy(classifier, downstream) 分组 + 下游收集 Map<K, D>
partitioningBy(predicate) 按布尔分区 Map<Boolean, List<T>>
collectingAndThen(c, finisher) 收集后二次转换 取决于 finisher

九、实战案例

案例 1:筛选及格学生并按分数降序

复制代码
class Student {
    String name;
    int score;
    // 构造方法、getter 省略
}

List<Student> students = Arrays.asList(
        new Student("张三", 85),
        new Student("李四", 58),
        new Student("王五", 92),
        new Student("赵六", 73)
);

List<String> topStudents = students.stream()
        .filter(s -> s.getScore() >= 60)                  // 及格
        .sorted((a, b) -> b.getScore() - a.getScore())   // 降序
        .map(Student::getName)                           // 只要名字
        .collect(Collectors.toList());
// 结果:[王五, 张三, 赵六]

案例 2:统计单词出现频率

复制代码
List<String> words = Arrays.asList("java", "python", "java", "c++", "python", "java");

Map<String, Long> freq = words.stream()
        .collect(Collectors.groupingBy(
                Function.identity(),
                Collectors.counting()
        ));
// 结果:{python=2, c++=1, java=3}

案例 3:基本类型流性能优化

复制代码
// 处理 int / long / double 时优先使用基本类型流,避免装箱开销
int sum = IntStream.range(1, 100)     // 1 到 99
        .filter(n -> n % 2 == 0)
        .sum();                       // 2450

十、传统写法 vs Stream 写法

需求:从一个整数列表中筛选出偶数,排序后取前 3 个。

传统写法

复制代码
List<Integer> result = new ArrayList<>();
for (Integer n : numbers) {
    if (n % 2 == 0) {
        result.add(n);
    }
}
Collections.sort(result);
List<Integer> top3 = result.subList(0, Math.min(3, result.size()));

Stream 写法

复制代码
List<Integer> top3 = numbers.stream()
        .filter(n -> n % 2 == 0)
        .sorted()
        .limit(3)
        .collect(Collectors.toList());

代码量减少约 60%,可读性和维护性大幅提升。

十一、注意事项

  1. Stream 不可复用 :一个 Stream 只能被消费一次,重复使用会抛出 IllegalStateException
  2. 避免副作用 :不要在 forEachpeek 中修改外部可变状态,保持函数式编程的纯粹性。
  3. 并行流谨慎使用parallelStream() 适合 CPU 密集型、数据量大的场景,但要注意线程安全问题。
  4. 空值处理 :配合 Optional 使用 findFirst / findAny 等方法,避免 NPE。
  5. 基本类型流 :处理 int / long / double 时优先使用 IntStream / LongStream / DoubleStream,避免装箱开销。
  6. distinct 依赖重写 :自定义对象去重需重写 equals()hashCode()
  7. sorted 稳定性 :Java 的 sorted() 是稳定排序,相等元素保持原顺序。
  8. toMap 重复 key:不传 mergeFunction 时遇到重复 key 会抛异常,务必指定合并策略。
相关推荐
wno7041 小时前
Spring Security权限控制
java·python·spring
杨运交2 小时前
[071][验证码模块]基于Spring拦截器的验证码认证设计思想
java·后端·spring
Geek-Chow2 小时前
CountDownLatch in Java
java
SL_staff3 小时前
从RBAC到场景化授权:《无忧·企业文档》三级权限模型的技术实践解析
java·开源·产品
传奇开心果编程3 小时前
【springboot基础语法学与练】第 1 课:从零开始
java·spring boot·后端·学习
SL_staff4 小时前
财务系统慎用低代码?从数据模型闭环看合规落地的技术实践
java·低代码·全栈
Bruce_Liuxiaowei4 小时前
Python 实例赋值遮蔽类属性:一个从不报错的静默陷阱
开发语言·python·语法糖
滕州市燕猫虎计算机科技工作室个体工商户4 小时前
IDEA:Command line is too long
java·ide·intellij-idea
步行cgn4 小时前
Spring p 命名空间注入详解
java·前端·spring