一、前言
在上一篇文章中,我们通过对比 JDK 8 前后的代码,直观感受到了 Stream API 带来的便捷性。但要真正用好 Stream,尤其是理解后续版本的优化点,你必须先吃透 JDK 8 作为 “奠基版本” 的核心用法 —— 这是所有 Stream 进阶的基础。
本文会从 Stream 创建、中间操作、终止操作、并行流 四个维度,全面解析 JDK 8 Stream 的核心用法,同时会明确指出每个环节中 JDK 8 存在的局限性,让你清楚后续版本的优化方向。
二、如何创建 Stream?
Stream 作为 “数据处理流水线”,第一步是从数据源创建 Stream。JDK 8 提供了 5 种核心创建方式,覆盖绝大多数场景:
1、从集合创建(最常用)
所有实现 Collection 接口的集合(List、Set、Queue 等)都内置了 stream() (串行流)和 parallelStream() (并行流)方法:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamCreateDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Stream");
list.add("JDK8");
// 串行流
Stream<String> serialStream = list.stream();
// 并行流
Stream<String> parallelStream = list.parallelStream();
}
}
2、从数组创建
通过 Arrays.stream() 方法,支持任意类型数组创建 Stream:
import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class ArrayStreamDemo {
public static void main(String[] args) {
// 对象数组
String[] strArray = {"a", "b", "c"};
Stream<String> strStream = Arrays.stream(strArray);
// 基本类型数组(有专用的 IntStream/LongStream/DoubleStream)
int[] intArray = {1, 2, 3};
IntStream intStream = Arrays.stream(intArray);
}
}
注意:JDK 8 中基本类型数组不会自动装箱为包装类型 Stream,而是提供了专用的 IntStream / LongStream 等,避免拆装箱性能损耗。
3、通过 Stream 静态方法创建
(1) Stream.of() :创建包含指定元素的 Streamjava
import java.util.stream.Stream;
public class StreamOfDemo {
public static void main(String[] args) {
// 单个元素
Stream<String> singleStream = Stream.of("Hello");
// 多个元素
Stream<Integer> multiStream = Stream.of(1, 2, 3, 4);
}
}
(2) Stream.empty() :创建空 Stream
用于处理 “无数据” 的场景,避免返回 null:
Stream<String> emptyStream = Stream.empty();
System.out.println(emptyStream.count()); // 输出:0
(3) Stream.iterate() :创建无限流(仅支持初始值 + 一元操作)
这是 JDK 8 的 核心局限性 之一: iterate 只能创建无限流,必须配合 limit() 才能变成有限流:
import java.util.stream.Stream;
public class StreamIterateDemo {
public static void main(String[] args) {
// JDK 8 写法:初始值 1,每次 +1(无限流)
Stream<Integer> infiniteStream = Stream.iterate(1, n -> n + 1);
// 必须用 limit 限制长度,否则会无限循环
infiniteStream.limit(5).forEach(System.out::println); // 输出 1,2,3,4,5
}
}
痛点:JDK 8 无法直接给 iterate 设置终止条件(比如 “生成小于 10 的数”),只能靠 limit 硬编码长度,灵活性差。
(4) Stream.generate() :创建无限流(通过 Supplier 生成)
适合生成随机数、常量等无规律的无限流,同样需要 limit 限制:
import java.util.Random;
import java.util.stream.Stream;
public class StreamGenerateDemo {
public static void main(String[] args) {
// 生成 3 个随机数
Stream<Double> randomStream = Stream.generate(() -> new Random().nextDouble());
randomStream.limit(3).forEach(System.out::println);
}
}
4、从文件创建(IO 流)
JDK 8 新增了 Files.lines() 方法,可读取文件的每一行作为 Stream 元素:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
public class FileStreamDemo {
public static void main(String[] args) {
try (Stream<String> lines = Files.lines(Paths.get("test.txt"))) {
// 读取文件并输出非空行
lines.filter(line -> !line.isEmpty()).forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
}
5、从函数式接口创建(小众)
通过 StreamSupport 配合 Spliterator 创建,适合自定义数据源(如自定义集合),日常开发极少用到,此处不展开。
三、核心中间操作
中间操作是 Stream 的 “加工环节”,特点是惰性求值 (不触发实际计算)、 返回新 Stream 、 可链式调用。JDK 8 提供的核心中间操作可分为 4 类:
1、 筛选与切片
|
按条件筛选元素 |
stream.filter(s -> s.length() > 3) |
|
去重(基于 equals 方法) |
stream.distinct() |
|
截取前 N 个元素 |
stream.limit(5) |
|
跳过前 N 个元素 |
stream.skip(2) |
JDK 8。
JDK 8 痛点:缺少 takeWhile() / dropWhile() 方法 —— 对于有序集合,无法 “按条件截取前缀” 或 “按条件丢弃前缀”,只能用 filter 遍历全部元素,效率低。
示例:筛选列表中长度大于 2 的字符串,去重后取前 2 个:
import java.util.Arrays;
import java.util.stream.Stream;
public class FilterDemo {
public static void main(String[] args) {
Stream<String> stream = Arrays.stream(new String[]{"a", "ab", "abc", "abc", "abcd"});
stream.filter(s -> s.length() > 2)
.distinct()
.limit(2)
.forEach(System.out::println); // 输出 abc, abcd
}
}
2、映射(转换元素)
|
将元素转换为另一种类型 |
stream.map(String::length) |
|
将每个元素转换为 Stream,再合并为一个 Stream |
stream.flatMap(s -> Arrays.stream(s.split(","))) |
|
转换为 IntStream(避免装箱) |
stream.mapToInt(String::length) |
示例:将字符串列
示例:将字符串列表转换为长度列表,再求和:
import java.util.Arrays;
import java.util.List;
public class MapDemo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "Stream", "JDK8");
int totalLength = list.stream()
.mapToInt(String::length) // 转换为 IntStream
.sum(); // 终止操作:求和
System.out.println(totalLength); // 输出:4+6+4=14
}
}
3、排序
|
自然排序(实现 Comparable 接口) |
stream.sorted() |
|
自定义排序 |
stream.sorted((a,b) -> b.compareTo(a)) |
示例:自定义排序(倒序):
import java.util.Arrays;
import java.util.List;
public class SortedDemo {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(3, 1, 2);
list.stream()
.sorted((a, b) -> b – a)
.forEach(System.out::println); // 输出 3,2,1
}
}
4、消费
peek(Consumer) :遍历元素但不修改,主要用于调试(查看中间结果):
import java.util.Arrays;
import java.util.List;
public class PeekDemo {
public static void main(String[] args) {
List<String> list = Arrays.asList("a", "b", "c");
list.stream()
.peek(s -> System.out.println("原始元素:" + s))
.map(s -> s.toUpperCase())
.peek(s -> System.out.println("转换后:" + s))
.forEach(System.out::println);
}
}
输出:
原始元素:a
转换后:A
A
原始元素:b
转换后:B
B
原始元素:c
转换后:C
C
四、核心终止操作
终止操作是 Stream 的 “成品输出环节”,特点是触发实际计算 、 消耗 Stream (执行后 Stream 不可复用)、 返回非 Stream 结果 (如集合、数值、void)。
1、遍历与匹配
|
遍历元素 |
void |
stream.forEach(System.out::println) |
|
按数据源顺序遍历(并行流中生效) |
void |
parallelStream.forEachOrdered(…) |
|
是否存在满足条件的元素 |
boolean |
stream.anyMatch(s -> s.contains("Java")) |
|
是否所有元素都满足条件 |
boolean |
stream.allMatch(s -> s.length() > 0) |
|
是否没有元素满足条件 |
boolean |
stream.noneMatch(s -> s.isEmpty()) |
|
获取第一个元素 |
Optional |
stream.findFirst().orElse("默认值") |
|
获取任意一个元素(并行流中效率高) |
Optional |
stream.findAny().orElse("默认值") |
示例:判断列表中是否有包含 “JD
示例:判断列表中是否有包含 “JDK” 的字符串:
import java.util.Arrays;import java.util.List;
public class MatchDemo {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "Stream", "JDK8");
boolean hasJdk = list.stream().anyMatch(s -> s.contains("JDK"));
System.out.println(hasJdk); // 输出 true
}
}
2、归约(聚合)
reduce() :将 Stream 元素聚合为一个值,是 sum() / max() / min() 的底层实现:
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class ReduceDemo {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
// 方式1:无初始值,返回 Optional
Optional<Integer> sum1 = list.stream().reduce((a, b) -> a + b);
System.out.println(sum1.get()); // 输出 10
// 方式2:有初始值,返回具体类型
int sum2 = list.stream().reduce(0, (a, b) -> a + b);
System.out.println(sum2); // 输出 10
// 求最大值
Optional<Integer> max = list.stream().reduce(Integer::max);
System.out.println(max.get()); // 输出 4
}
}
3、收集(转换为集合 / 其他类型)
collect(Collector) :最常用的终止操作,JDK 8 提供了 Collectors 工具类封装常用收集器:
|
转换为 List(默认 ArrayList) |
stream.collect(Collectors.toList()) |
|
转换为 Set(默认 HashSet) |
stream.collect(Collectors.toSet()) |
|
转换为 Map(需处理重复键) |
stream.collect(Collectors.toMap(User::getId, User::getName)) |
|
拼接字符串 |
stream.collect(Collectors.joining(",")) |
|
分组 |
stream.collect(Collectors.groupingBy(User::getAge)) |
|
统计(计数、求和、最大 / 最小 / 平均值) |
stream.collect(Collectors.summarizingInt(User::getAge)) |
JDK 8 痛点:
Col数;
JDK 8 痛点:
Collectors.toList()返回的是可变 ArrayList ,创建不可变列表需要手动封装(如 Collections.unmodifiableList() );
无直接的 toMap() 重复键处理简化方案,需手动指定合并函数;
无法同时执行多个收集器(如一次遍历同时求最大值和最小值)。
示例:将用户列表按年龄分组,拼接每组的用户名:
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class CollectDemo {
static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
public static void main(String[] args) {
List<User> userList = Arrays.asList(
new User("Alice", 20),
new User("Bob", 20),
new User("Charlie", 25)
);
// 按年龄分组,拼接每组用户名
Map<Integer, String> ageGroup = userList.stream()
.collect(Collectors.groupingBy(
User::getAge,
Collectors.mapping(User::getName, Collectors.joining(","))
));
System.out.println(ageGroup); // 输出 {20=Alice,Bob, 25=Charlie}
}
}
4、数值计算(专用流)
IntStream / LongStream / DoubleStream 提供了便捷的数值计算方法:
import java.util.Arrays;
import java.util.List;
public class NumericDemo {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
int sum = list.stream().mapToInt(Integer::intValue).sum(); // 求和
long count = list.stream().count(); // 计数
double avg = list.stream().mapToInt(Integer::intValue).average().getAsDouble(); // 平均值
int max = list.stream().mapToInt(Integer::intValue).max().getAsInt(); // 最大值
System.out.println("和:" + sum + ",计数:" + count + ",平均值:" + avg + ",最大值:" + max);
// 输出:和:10,计数:4,平均值:2.5,最大值:4
}
}
五、并行流:便捷但有坑
JDK 8 最亮眼的特性之一是并行流( parallelStream() ),无需手动管理线程池,即可实现数据并行处理:
import java.util.Arrays;
import java.util.List;
public class ParallelStreamDemo {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// 并行流求和
int sum = list.parallelStream().mapToInt(Integer::intValue).sum();
System.out.println(sum); // 输出 15
}
}
JDK 8 并行流的核心坑点:
1.线程安全问题:如果在并行流中修改共享变量(如 ArrayList ),会导致数据错乱:
// 错误示例:并行流操作非线程安全集合
List<Integer> result = new ArrayList<>();
List<Integer> list = Arrays.asList(1, 2, 3, 4);
list.parallelStream().forEach(result::add);
System.out.println(result.size()); // 可能不是 4,数据错乱
2.性能未必最优:并行流基于 Fork/Join 框架,小数据量场景下,线程切换开销大于并行收益;
3.顺序不可控:默认 forEach 不保证顺序,需用 forEachOrdered (但会损失并行性能);
4.JDK 8 优化不足:后续版本(如 JDK 11+)对并行流的性能和稳定性做了大量优化。
六、核心局限性
学完 JDK 8 Stream 的核心用法,我们再梳理一遍它的核心痛点(也是后续版本的优化方向):
创建环节: Stream.iterate() 仅支持无限流,无终止条件;无 ofNullable() 方法,处理 null 易抛 NPE;
中间操作:缺少 takeWhile() / dropWhile() ,有序集合条件截取效率低;
终止操作: Collectors.toList() 返回可变列表;无简洁的不可变集合收集器;无法同时执行多个收集器;
并行流:性能和稳定性不足,线程安全问题易踩坑;
其他:Stream 不可复用,关闭逻辑不完善。
这些痛点,正是 JDK 9、11、17 版本对 Stream API 进行增强的核心动机 —— 下一篇文章,我们会重点讲解 JDK 9 对 Stream 的首次增强,看看这些新特性如何解决上述痛点。
七、总结
JDK 8 Stream 的核心流程是 “创建 → 中间操作 → 终止操作”,中间操作惰性求值,终止操作触发计算;
JDK 8 提供了丰富的创建方式和操作方法,覆盖绝大多数集合处理场景,但存在诸多局限性;
并行流虽便捷,但 JDK 8 版本存在线程安全、性能优化不足等问题,使用时需谨慎。
掌握这些基础用法,是理解后续版本差异的关键 —— 下一篇,我们将聚焦 JDK 9,看看它如何填补 JDK 8 Stream 的这些 “漏洞”。


