
在 Java 8 引入的众多特性中,Stream(流)无疑是最具变革性的之一。它让我们能以声明式、函数式的方式处理集合数据------无需显式循环,就能完成过滤、映射、聚合等操作。
我们可以将 Java 流看作是一个数据流经的管道。无需手动编写循环和条件语句来处理列表,只需告诉 Java 对每个元素应执行什么操作,Java 流 API 会负责处理内部的实现方式。
Java 流并不存储数据。相反,它对诸如 List、Set、Map 或数组等现有的数据源进行操作。流会对数据源应用一系列操作。
我们将向你介绍 Java 流,学习如何从 Java 集合创建流,首次了解流管道,并了解 Lambda 表达式、方法引用和其他函数式编程元素如何与 Java 流协同工作。还将学习如何将收集器和可选链与 Java 流结合使用,以及在程序中何时应该使用或不应该使用流。
总之,本文将带你轻松入门 Java 流,从创建到组合,用简洁优雅的代码释放数据处理的真正潜力。
流 VS 集合
许多开发者会被 Java 流和 Java 集合之间的区别所困扰:
- 集合(如
ArrayList或HashSet)用于存储。它们将数据保存在内存中以供你访问。 - 流关注的是行为。它们描述对数据要做什么,而不是如何存储数据。
打个比方,可以把集合想象成存放食材的橱柜,而流则是将这些食材做成一顿饭的食谱。
流通过描述要做什么而不是如何去做,赋予 Java 一种函数式和声明式的特性。
为什么使用流
Java 开发者会出于多种原因偏向并使用流:
- 代码更简洁,可替代嵌套循环和条件语句。
- 样板代码更少,无需再编写手动的
for循环。 - 逻辑更具可读性,流管道读起来就像自然语言。
通过比较循环和流,我们就能初步看出这些差异。
在 Java 中,流常常会取代传统的循环,一旦你开始使用流,就很难再回头使用传统方式了。下面是一个典型的 for 循环示例:
java
List<String> names = List.of("patrick", "mike", "james", "bill");
List<String> result = new ArrayList<>();
for (String name : names) {
if (name.length() > 4) {
result.add(name.toUpperCase());
}
}
Collections.sort(result);
System.out.println(result);
如果使用流呢?
java
List<String> names = List.of("patrick", "mike", "james", "bill");
List<String> result = names.stream()
.filter(name -> name.length() > 4)
.map(String::toUpperCase)
.sorted()
.toList();
System.out.println(result);
与循环不同,流的操作语句读起来几乎就像英语:"取出名字,按长度过滤,转换为大写,进行排序,然后收集到一个列表中"。 操作完成后,输出将是:[james, patrick]。
从集合创建流

流可以从多种来源开始。把下面所有的示例都看作是 "打开水龙头" 的方式。
以下是如何从集合(在这个例子中是一个包含名字的列表)创建流的方法:
java
List<String> names = List.of("James", "Bill", "Patrick");
Stream<String> nameStream = names.stream();
以下是如何从 Map 创建流的方法:
java
Map<Integer, String> idToName = Map.of(1, "James", 2, "Bill");
Stream<Map.Entry<Integer, String>> entryStream = idToName.entrySet().stream();
从数组创建流:
java
String[] names = {"James", "Bill", "Patrick"};
Stream<String> nameStream = Arrays.stream(names);
当然,也可以使用 Stream.of() 创建流:
java
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);
使用 Stream.of(),你可以传入任何类型的值或对象来创建一个流。
当你手头没有集合或数组时,这是一种快速创建流的简便方法。对于小型且固定的数据集合或快速测试来说十分适用。
Stream.generate() 创建流
Stream.generate() 方法创建一个无限流。只要管道需要,它就会持续生成值:
java
Stream.generate(() -> "hello").forEach(System.out::println);
这个流永远不会停止。使用 limit() 方法来控制它:
java
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
Stream.generate() 和 Stream.iterate() 都可以生成无限序列。一定要使用限制操作或短路操作,避免无限执行。
如果你需要安全地返回一个空流而不是 null,可以使用 Stream.empty():
java
Stream<String> emptyStream = Stream.empty();
这样做避免了空指针检查,并使返回流的方法更安全、更简洁。
流操作

流有中间(延迟)操作和终端(执行)操作。这两种类型的操作共同构成了数据管道。
中间操作(途中转换)
中间流操作不会立即触发执行。它们只是在过程中添加处理步骤:
map:转换每个元素。filter:仅保留符合条件的元素。sorted:对元素进行排序。distinct:去除重复项。limit/skip:裁剪流。flatMap:将嵌套结构(例如列表的列表)扁平化为一个流。peek:让你在元素流过时查看它们(非常适合调试以及记录日志)。takeWhile:持续提取元素,直到条件为假(类似于有条件的限制)。dropWhile:在条件为真时跳过元素,然后保留其余元素。
流是惰性的
流首先会准备好所有步骤(过滤、映射、排序),但在终端操作触发处理之前,不会有任何实际操作发生。这种惰性求值机制通过仅处理所需的数据,提高了流操作的效率。
以这个流管道为例:
java
List<String> names = List.of("james", "bill", "patrick", "guy", "bob");
names.stream()
.filter(n -> n.length() > 3) // 保留长度超过3个字符的名字
.map(String::toUpperCase) // 转换为大写
.sorted(); // 按照字母顺序排序
System.out.println("List result: " + names);
结果是: [james, bill, patrick, guy, bob]。
乍一看,这个流管道似乎应该:
- 过滤掉
"guy"和"bob"(因为它们的长度不大于3)。 - 将其余的转换为大写。
- 对它们进行排序。
但实际上,这个管道什么都没有做。
原因是 Java 中的流是惰性的:
- 所有这些调用(
filter、map、sorted)都是中间操作。 - 它们不会立即执行。相反,它们只记录操作计划。
- 只有当你添加一个终端操作,如
.toList()、forEach()或count()时,这个计划才会运行。
由于上述代码中没有终端操作,该流管道被丢弃,原始列表将保持不变地打印出来。
终端操作(上菜)
现在我们可以来看看流的第二类操作。终端操作会触发流运行并产生一个结果:
forEach():对每个元素执行某些操作。collect():将元素收集到一个集合中。toList():将所有元素收集到一个不可变的列表中(Java 16 及以上版本)。reduce():将多个元素合并为单个结果(如求和、求积等)。count():元素有多少个?findFirst():返回第一个符合过滤条件的元素(在顺序很重要的情况下很有用)。findAny():返回任何一个匹配的元素(在不保证顺序的并行流中特别有用)。toArray():将结果收集到一个数组中。min(Comparator) / max(Comparator):根据一个比较器找到最小或最大的元素。anyMatch(predicate):是否有任何元素匹配?allMatch(predicate):所有元素都匹配吗?noneMatch(predicate):没有元素匹配吗?
下面是一个使用终端操作的流的示例:
java
List<String> names = List.of("james", "bill", "patrick", "guy");
List<String> result = names.stream()
.filter(n -> n.length() > 3)
.map(String::toUpperCase)
.sorted()
.toList(); // 终端操作方法在这里触发执行
System.out.println(result);
上述代码输出将是:[BILL, JAMES, PATRICK]。
流是一次性的
一旦流被处理,它就会被消耗掉且不能再被重复使用。终端操作会关闭流:
java
List<String> names = List.of("James", "Bill", "Patrick");
Stream<String> s = names.stream();
s.forEach(System.out::println); // OK
s.count(); // IllegalStateException ------ 已被处理
在这段代码中,第一次调用会使所有数据通过流水线,之后流就会被关闭。如果需要,请创建一个新的流:
java
long count = names.stream().count(); // 正确做法:新的流实例
流管道
我们给出一个包含中间操作和终态操作的流管道示例:
java
List<String> result = names.stream() // 数据源
.filter(n -> n.length() > 3) // 中间操作
.map(String::toUpperCase) // 中间操作
.sorted() // 中间操作
.toList(); // 终端操作
与集合共舞

除了流,Java 8 还引入了收集器,可用于描述如何收集处理后的数据。
收集到列表会创建一个新的、不可修改的、包含长度超过三个字符的名称的列表。不可变的结果使流代码更安全且更具函数式编程风格:
java
List<String> list = names.stream ()
.filter (n -> n.length () > 3)
.toList (); // Java 16 及以上版本可用
在这里,我们将结果收集到一个集合中,这样会自动去除重复项。当唯一性比顺序更重要时,就使用集合:
java
Set<String> set = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toSet());
在这里,我们将数据收集到一个 Map 中,其中键是字符串的长度,值是 name 本身:
java
Map<Integer, String> map = names.stream()
.collect(Collectors.toMap(
String::length,
n -> n
));
如果多个名称的长度相同,就会发生冲突。可以使用合并函数来处理这种情况:
java
Map<Integer, String> safeMap = names.stream()
.collect(Collectors.toMap(
String::length,
n -> n,
(a, b) -> a // 如果键发生冲突,保留第一个值。
));
连接字符串
Collectors.joining() 可以使用你选择的任何分隔符将流中的所有元素合并为一个字符串。你可以使用 |、;,甚至 \n:
java
List<String> names = List.of("Bill", "James", "Patrick");
String result = names.stream()
.map(String::toUpperCase)
.collect(Collectors.joining(", "));
System.out.println(result);
输出为:BILL, JAMES, PATRICK。
数据分组
Collectors.groupingBy() 按键(这里是字符串长度)对元素进行分组,并返回一个 Map<Key,List<Value>>:
java
List<String> names = List.of("james", "linus", "john", "bill", "patrick");
Map<Integer, List<String>> grouped = names.stream()
.collect(Collectors.groupingBy(String::length));
输出为:{4=[john, bill], 5=[james, linus], 7=[patrick]}。
数字汇总
你也可以使用集合进行汇总:
ini
List<Integer> numbers = List.of(3, 5, 7, 2, 10);
IntSummaryStatistics stats = numbers.stream()
.collect(Collectors.summarizingInt(n -> n));
System.out.println(stats);
输出为:IntSummaryStatistics {count=5, sum=27, min=2, average=5.4, max=10}。
如果你只想要平均值,可以这样做:
java
double avg = numbers.stream()
.collect(Collectors.averagingDouble(n -> n));
函数式编程

前面提到过,流融合了函数式和声明式的元素。下面我们来看看流中的一些函数式编程元素。
Lambda 表达式与方法引用
Lambda 表达式可以在内部定义行为,而方法引用则可以重用现有的方法:
java
names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.forEach(System.out::println);
map() vs flatMap()
根据经验来说:
- 当你有一个输入并且想要一个输出时,使用
map()方法。 - 当你有一个输入并且想要多个输出(展平后的)时,使用
flatMap()方法。
以下是一个在流中使用 map() 方法的示例:
java
List<List<String>> nested = List.of(
List.of("james", "bill"),
List.of("patrick")
);
nested.stream()
.map(list -> list.stream())
.forEach(System.out::println);
输出会是:
plain
java.util.stream.ReferencePipeline$Head@5ca881b5
java.util.stream.ReferencePipeline$Head@24d46ca6
有两行是因为有两个内部列表,所以你需要两个 Stream 对象。另外要注意哈希值会有所不同。
如果使用 flatMap() 呢:
java
nested.stream()
.flatMap(List::stream)
.forEach(System.out::println);
输出如下:
plain
james
bill
patrick
对于更深层次的嵌套:
java
List<List<List<String>>> deep = List.of(
List.of(List.of("James", "Bill")),
List.of(List.of("Patrick"))
);
List<String> flattened = deep.stream()
.flatMap(List::stream)
.flatMap(List::stream)
.toList();
System.out.println(flattened);
输出为:[James, Bill, Patrick]。
Optional 操作符
Optional 操作符是另一种可与流结合使用的有用操作:
java
List<String> names = List.of("James", "Bill", "Patrick");
String found = names.stream()
.filter(n -> n.length() > 6)
.findFirst()
.map(String::toUpperCase)
.orElse("NOT FOUND");
System.out.println(found);
输出会是:NOT FOUND。
findFirst() 会返回一个 Optional 对象,它可以安全地表示一个可能不存在的值。如果没有匹配项,.orElse() 会提供一个备用值。出于同样的原因,诸如 findAny()、min() 和 max() 等方法也会返回 Optional 对象。
总结
Java 的 Stream API 彻底改变了我们处理数据的方式。你只需声明"要做什么"------比如过滤、映射或排序------具体的实现细节就交给 Java 高效地去完成。
将 Stream、Collector 和 Optional 结合使用,能让现代 Java 代码变得更简洁、清晰,也更健壮。当你需要对集合进行转换或分析时,Stream 是理想的选择;但如果是涉及索引操作或频繁变动的任务,传统方式可能更合适。
一旦你真正掌握了 Stream 的用法,恐怕就很难再回到手写循环的老路了。在熟悉本文介绍的基础知识后,不妨进一步探索并行流、基本类型专用流(如 IntStream)以及自定义 Collector 等高级特性。最重要的是多动手实践------试着运行和修改文中的示例代码,只有通过实际操作,才能真正内化这些技能。