Рубрики
Без рубрики

Как получить доступ к счетчику итераций в каждом цикле

Java для каждого цикла не предоставляет счетчик итераций. Существует несколько способов перебора как элемента, так и его индекса.

Автор оригинала: Ashley Frieze.

1. Обзор

При итерации данных в Java мы можем получить доступ как к текущему элементу, так и к его положению в источнике данных.

Этого очень легко достичь в классическом цикле for , где положение обычно является фокусом вычислений цикла, но это требует немного больше работы, когда мы используем конструкции, такие как для каждого цикла или потока.

В этом коротком уроке мы рассмотрим несколько способов, которыми для каждой операции может включать счетчик.

2. Реализация счетчика

Давайте начнем с простого примера. Мы возьмем упорядоченный список фильмов и выведем их с их рейтингом.

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

2.1. для цикла

Цикл for использует счетчик для ссылки на текущий элемент, поэтому это простой способ работы как с данными, так и с их индексом в списке:

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

Поскольку этот List , вероятно, является ArrayList , операция get эффективна, и приведенный выше код является простым решением нашей проблемы.

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

Однако не все источники данных в Java могут быть повторены таким образом. Иногда get является трудоемкой операцией, или мы можем обработать только следующий элемент источника данных с помощью Stream или Iterable.

2.2. для Каждого Цикла

Мы продолжим использовать наш список фильмов, но давайте представим, что мы можем только перебирать его, используя Java для каждой конструкции:

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. Функционал для каждого

Написание расширения счетчика каждый раз, когда нам это нужно, может привести к дублированию кода и может привести к случайным ошибкам, связанным с обновлением переменной счетчика. Поэтому мы можем обобщить вышесказанное, используя функциональные интерфейсы Java .

Во-первых, мы должны думать о поведении внутри цикла как о потребителе как элемента в коллекции, так и индекса. Это можно смоделировать с помощью Consumer , который определяет функцию accept , которая принимает два параметра

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

Поскольку внутри нашего цикла используется два значения, мы могли бы написать общую операцию цикла. Это может занять Итерируемый исходных данных, над которыми будет выполняться цикл for each, и Двоичный для выполнения операции над каждым элементом и его индексом. Мы можем сделать это общим с параметром типа T :

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

Мы можем использовать это в нашем примере ранжирования фильмов, предоставив реализацию для BiConsumer в качестве лямбды:

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

4. Добавление счетчика в forEach с потоком

API Java Stream позволяет нам выразить, как наши данные проходят через фильтры и преобразования. Он также предоставляет функцию forEach . Давайте попробуем преобразовать это в операцию, включающую счетчик.

Функция Stream forEach принимает Consumer для обработки следующего элемента. Тем не менее, мы могли бы создать этот Потребитель , чтобы отслеживать счетчик и передавать элемент на BiConsumer :

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

Эта функция возвращает новую лямбду. Эта лямбда использует объект AtomicInteger для отслеживания счетчика во время итерации. Функция getAndIncrement вызывается каждый раз, когда появляется новый элемент.

Лямбда, созданная этой функцией, делегируется переданному Потребителю , чтобы алгоритм мог обрабатывать как элемент, так и его индекс.

Давайте посмотрим, как это используется в нашем примере ранжирования фильмов против потока под названием фильмы :

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

Внутри forEach находится вызов функции with Counter для создания объекта, который одновременно отслеживает счетчик и действует как Потребитель , которому операция forEach также передает свои значения.

5. Заключение

В этой короткой статье мы рассмотрели три способа прикрепления счетчика к Java для каждой операции.

Мы видели, как отслеживать индекс текущего элемента на каждой реализации их для цикла. Затем мы рассмотрели, как обобщить этот шаблон и как добавить его в потоковые операции.

Как всегда, пример кода для этой статьи доступен на GitHub .