Как да получите достъп до брояч на итерации във всеки цикъл

1. Общ преглед

Докато итерираме по данни в Java, може да пожелаем достъп до текущия елемент и до неговата позиция в източника на данни.

Това е много лесно да се постигне в класически цикъл for , където позицията обикновено е във фокуса на изчисленията на цикъла, но изисква малко повече работа, когато използваме конструкции като за всеки цикъл или поток.

В този кратък урок ще разгледаме няколко начина, които за всяка операция могат да включват брояч.

2. Внедряване на брояч

Нека започнем с прост пример. Ще вземем подреден списък с филми и ще ги изведем с тяхното класиране.

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

2.1. за Loop

А за контур използва брояч за обръщение към текущия елемент, така че това е един лесен начин да работи както над данните и неговия индекс в списъка:

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

Тъй като този списък вероятно е 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.

Първо, трябва да мислим за поведението в цикъла като потребител както на елемента в колекцията, така и на индекса. Това може да се моделира с помощта на BiConsumer , който дефинира функция за приемане, която приема два параметъра

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

Тъй като вътрешността на нашия цикъл е нещо, което използва две стойности, бихме могли да напишем обща циклична операция. Може да отнеме Iterable на изходните данни, над които ще се изпълнява всеки цикъл, и BiConsumer за операцията, която трябва да се извърши върху всеки елемент и неговия индекс. Можем да направим това родово с параметър тип 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 отнема от потребителя да обработи следващия елемент. Можем обаче да създадем този потребител, за да следи брояча и да предава елемента на BiConsumer :

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

Тази функция връща нова ламбда. Тази ламбда използва обекта AtomicInteger, за да следи брояча по време на итерация. Функцията getAndIncrement се извиква всеки път, когато има нов елемент.

Създадената от тази функция ламбда делегира на предадения BiConsumer, така че алгоритъмът да може да обработва както елемента, така и неговия индекс.

Нека да видим това в нашия пример за класиране на филми срещу поток, наречен филми :

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

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

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

В тази кратка статия разгледахме три начина да прикачите брояч към Java за всяка операция.

Видяхме как да проследим индекса на текущия елемент при всяка имплементация за цикъл. След това разгледахме как да обобщим този модел и как да го добавим към поточни операции.

Както винаги примерният код за тази статия е достъпен в GitHub.