1. 概述
在 Java 中遍历数据时,我们常常需要同时访问当前元素及其在数据源中的位置。对于传统的 for
循环来说,这很容易实现,因为它的索引本身就是循环的核心部分;但在使用 for-each
或 Stream
等现代语法结构时,要获取这个计数器就需要额外处理。
本文将介绍几种在 for-each
遍历过程中添加计数器的方式。
2. 实现计数器
我们以一个简单的例子开始:有一个电影列表,我们要输出每部电影的排名。
List<String> IMDB_TOP_MOVIES = Arrays.asList(
"The Shawshank Redemption",
"The Godfather",
"The Godfather II",
"The Dark Knight"
);
2.1. 使用传统 for 循环
传统 for
循环本身就使用索引来引用当前项,因此非常适合同时操作数据和索引:
List<String> rankings = new ArrayList<>();
for (int i = 0; i < movies.size(); i++) {
String ranking = (i + 1) + ": " + movies.get(i);
rankings.add(ranking);
}
✅ 如果是 ArrayList
,get()
方法效率很高,这种方式简单粗暴又高效。
assertThat(getRankingsWithForLoop(IMDB_TOP_MOVIES))
.containsExactly(
"1: The Shawshank Redemption",
"2: The Godfather",
"3: The Godfather II",
"4: The Dark Knight"
);
⚠️ 但不是所有数据源都支持通过索引访问,有些可能只支持顺序读取(如迭代器或流),这时候就不能用 get()
。
2.2. 使用 for-each 循环
继续使用上面的电影列表,假设我们只能通过 for-each
来遍历它:
for (String movie : IMDB_TOP_MOVIES) {
// use movie value
}
此时我们需要手动维护一个计数器变量,在循环外部初始化,在循环内部递增:
int i = 0;
for (String movie : movies) {
String ranking = (i + 1) + ": " + movie;
rankings.add(ranking);
i++;
}
⚠️ 注意:必须在使用完计数器后再进行自增操作,否则会偏移一位。
3. 函数式风格的 for-each 带计数器
每次都手动维护计数器不仅麻烦,还容易出错。我们可以借助 Java 8 的函数式接口来封装通用逻辑。
首先定义行为:循环体内的操作应该是一个接受索引和元素两个参数的消费者。这正好可以用 BiConsumer<T, U>
表示:
@FunctionalInterface
public interface BiConsumer<T, U> {
void accept(T t, U u);
}
基于此,我们可以编写一个通用方法,接收一个 Iterable
和一个 BiConsumer
,并在内部维护计数器:
static <T> void forEachWithCounter(Iterable<T> source, BiConsumer<Integer, T> consumer) {
int i = 0;
for (T item : source) {
consumer.accept(i, item);
i++;
}
}
然后就可以这样调用:
List<String> rankings = new ArrayList<>();
forEachWithCounter(movies, (i, movie) -> {
String ranking = (i + 1) + ": " + movie;
rankings.add(ranking);
});
✅ 这样做的好处是逻辑清晰、复用性强,避免了手动管理计数器带来的风险。
4. 在 Stream 的 forEach 中加入计数器
Java 的 Stream API 提供了强大的链式操作能力,其中也包括 forEach
方法。我们也可以给它加上计数器功能。
Stream.forEach
接收的是一个 Consumer<T>
,但我们可以通过包装器让它也能接收索引:
public static <T> Consumer<T> withCounter(BiConsumer<Integer, T> consumer) {
AtomicInteger counter = new AtomicInteger(0);
return item -> consumer.accept(counter.getAndIncrement(), item);
}
该方法返回一个新的 Consumer
,它利用 AtomicInteger
来保证线程安全地记录索引,并在每次处理元素时递增。
使用方式如下:
List<String> rankings = new ArrayList<>();
movies.forEach(withCounter((i, movie) -> {
String ranking = (i + 1) + ": " + movie;
rankings.add(ranking);
}));
✅ 这样既保留了 Stream 的链式风格,又实现了带索引的操作。
5. 总结
在这篇文章中,我们探讨了三种为 Java 的 for-each
循环添加计数器的方法:
- 传统
for
循环:天然带索引,适合随机访问的数据结构。 - 手动维护计数器:适用于
for-each
,但要注意更新时机。 - 封装为函数式工具:提高代码复用性,减少错误。
此外,我们也展示了如何在 Stream 中优雅地加入索引支持,使得整个流程更加现代化且易于扩展。
一如既往,文中示例代码可以在 GitHub 仓库 找到。