Перейти к основному содержимому

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

· 4 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Обзор

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

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

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

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

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

List<String> 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);
}

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

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 .

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

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

Поскольку внутри нашего цикла используются два значения, мы могли бы написать общую операцию цикла. Он может принимать 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++;
}
}

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

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

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

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

Функция 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);
}

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

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

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

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

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

5. Вывод

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

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

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