1. 概述

在 Java 中遍历数据时,我们常常需要同时访问当前元素及其在数据源中的位置。对于传统的 for 循环来说,这很容易实现,因为它的索引本身就是循环的核心部分;但在使用 for-eachStream 等现代语法结构时,要获取这个计数器就需要额外处理。

本文将介绍几种在 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);
}

✅ 如果是 ArrayListget() 方法效率很高,这种方式简单粗暴又高效。

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 仓库 找到。


原始标题:How to Access an Iteration Counter in a For Each Loop

« 上一篇: Java周报,361