How to Access an Iteration Counter in a For Each Loop – 如何在For Each循环中访问一个迭代计数器

最后修改: 2020年 11月 25日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

While iterating over data in Java, we may wish to access both the current item and its position in the data source.

在Java中迭代数据时,我们可能希望同时访问当前项目和它在数据源中的位置。

This is very easy to achieve in a classic for loop, where the position is usually the focus of the loop’s calculations, but it requires a little more work when we use constructs like for each loop or stream.

这在经典的for循环中很容易实现,因为位置通常是循环计算的重点,但当我们使用for each循环或流等结构时,需要多做一些工作。

In this short tutorial, we’ll look at a few ways that for each operation can include a counter.

在这个简短的教程中,我们将看看for每个操作可以包含一个计数器的几种方法。

2. Implementing a Counter

2.实施一个计数器

Let’s start with a simple example. We’ll take an ordered list of movies and output them with their ranking.

让我们从一个简单的例子开始。我们将采取一个有序的电影列表,并输出它们的排名。

List<String> IMDB_TOP_MOVIES = Arrays.asList("The Shawshank Redemption",
  "The Godfather", "The Godfather II", "The Dark Knight");

2.1. for Loop

2.1. for循环

A for loop uses a counter to reference the current item, so it’s an easy way to operate over both the data and its index in the list:

for 循环使用一个计数器来引用当前的项目,所以它是对数据和它在列表中的索引进行操作的一种简单方法。

List rankings = new ArrayList<>();
for (int i = 0; i < movies.size(); i++) {
    String ranking = (i + 1) + ": " + movies.get(i);
    rankings.add(ranking);
}

As this List is probably an ArrayList, the get operation is efficient, and the above code is a simple solution to our problem.

由于这个List可能是一个ArrayListget操作是有效的,上述代码是对我们问题的简单解决。

assertThat(getRankingsWithForLoop(IMDB_TOP_MOVIES))
  .containsExactly("1: The Shawshank Redemption",
      "2: The Godfather", "3: The Godfather II", "4: The Dark Knight");

However, not all data sources in Java can be iterated over this way. Sometimes get is a time-intensive operation, or we can only process the next element of a data source using Stream or Iterable.

然而,并不是Java中的所有数据源都能以这种方式进行迭代。有时get是一个耗时的操作,或者我们只能使用StreamIterable.处理数据源的下一个元素。

2.2. for Each Loop

2.2.for Each循环

We’ll continue using our list of movies, but let’s pretend that we can only iterate over it using Java’s for each construct:

我们将继续使用我们的电影列表,但让我们假设我们只能使用Java的for each结构对其进行迭代。

for (String movie : IMDB_TOP_MOVIES) {
   // use movie value
}

Here we need to use a separate variable to track the current index. We can construct that outside of the loop, and increment it inside:

这里我们需要使用一个单独的变量来跟踪当前的索引。我们可以在循环外构建该变量,并在循环内将其递增。

int i = 0;
for (String movie : movies) {
    String ranking = (i + 1) + ": " + movie;
    rankings.add(ranking);

    i++;
}

We should note that we have to increment the counter after it has been used within the loop.

我们应该注意的是,我们必须在循环内使用计数器后递增它

3. A Functional for Each

3.职能部门每个人服务

Writing the counter extension every time we need it might result in code duplication and may risk accidental bugs concerning when to update the counter variable. We can, therefore, generalize the above using Java’s functional interfaces.

在我们每次需要的时候编写计数器扩展可能会导致代码的重复,并且可能会有意外的错误,涉及到何时更新计数器变量。因此,我们可以使用Java的functional interfaces来概括上述内容。

First, we should think of the behavior inside the loop as a consumer of both the item in the collection and also the index. This can be modeled using BiConsumer, which defines an accept function that takes two parameters

首先,我们应该将循环内的行为视为集合中的项目和索引的消费者。这可以使用BiConsumer来建模,它定义了一个accept函数,它需要两个参数

@FunctionalInterface
public interface BiConsumer<T, U> {
   void accept(T t, U u);
}

As the inside of our loop is something that uses two values, we could write a general looping operation. It could take the Iterable of the source data, over which the for each loop will run, and the BiConsumer for the operation to perform on each item and its index. We can make this generic with the type parameter T:

由于我们的循环的内部是使用两个值的东西,我们可以写一个一般的循环操作。它可以接受源数据的Iterable,for each循环将在其上运行,而BiConsumer用于对每个项目及其索引执行操作。我们可以通过类型参数T使其通用。

static <T> void forEachWithCounter(Iterable<T> source, BiConsumer<Integer, T> consumer) {
    int i = 0;
    for (T item : source) {
        consumer.accept(i, item);
        i++;
    }
}

We can use this with our movie rankings example by providing the implementation for the BiConsumer as a lambda:

我们可以通过将BiConsumer的实现作为一个lambda来使用我们的电影排名的例子。

List rankings = new ArrayList<>();
forEachWithCounter(movies, (i, movie) -> {
    String ranking = (i + 1) + ": " + movies.get(i);
    rankings.add(ranking);
});

4. Adding a Counter to forEach with Stream

4.用StreamforEach添加一个计数器

The Java Stream API allows us to express how our data passes through filters and transformations. It also provides a forEach function. Let’s try to convert that into an operation that includes the counter.

Java Stream API允许我们表达我们的数据如何通过过滤器和转换。它还提供了一个forEach函数。让我们试着将其转换为包括计数器的操作。

The Stream forEach function takes a Consumer to process the next item. We could, however, create that Consumer to keep track of the counter and pass the item onto a BiConsumer:

Stream forEach函数需要一个Consumer来处理下一个项目。然而,我们可以创建该Consumer来跟踪计数器,并将该项目传递给BiConsumer

public static <T> Consumer<T> withCounter(BiConsumer<Integer, T> consumer) {
    AtomicInteger counter = new AtomicInteger(0);
    return item -> consumer.accept(counter.getAndIncrement(), item);
}

This function returns a new lambda. That lambda uses the AtomicInteger object to keep track of the counter during iteration. The getAndIncrement function is called every time there’s a new item.

这个函数返回一个新的lambda。这个lambda使用AtomicInteger对象来跟踪迭代过程中的计数器。每当有一个新项目时,getAndIncrement函数就会被调用。

The lambda created by this function delegates to the BiConsumer passed in so that the algorithm can process both the item and its index.

这个函数创建的lambda委托给传递进来的BiConsumer,以便该算法可以同时处理项目和它的索引。

Let’s see this in use by our movie ranking example against a Stream called movies:

让我们看看我们的电影排名例子对一个叫做moviesStream的使用情况。

List rankings = new ArrayList<>();
movies.forEach(withCounter((i, movie) -> {
    String ranking = (i + 1) + ": " + movie;
    rankings.add(ranking);
}));

Inside the forEach is a call to the withCounter function to create an object which both tracks the count and acts as the Consumer that the forEach operation passes its values too.

forEach中,是对withCounter函数的调用,以创建一个对象,它既跟踪计数,又充当ConsumerforEach操作将其值传给它。

5. Conclusion

5.总结

In this short article, we’ve looked at three ways to attach a counter to Java for each operation.

在这篇短文中,我们看了三种将计数器附加到Javafor每个操作的方法。

We saw how to track the index of the current item on each implementation of them for a loop. We then looked at how to generalize this pattern and how to add it to streaming operations.

我们看到了如何在它们的每个实现上跟踪当前项目的索引for一个循环。然后我们看了如何泛化这种模式,以及如何将其添加到流式操作中。

As always the example code for this article is available over on GitHub.

像往常一样,本文的示例代码可以在GitHub上找到