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 .