ForEachループで反復カウンターにアクセスする方法
1. 概要
Javaでデータを反復処理しているときに、現在のアイテムとデータソース内のその位置の両方にアクセスしたい場合があります。
これは、通常は位置がループの計算の焦点となる従来の for ループで非常に簡単に実現できますが、各ループやストリームなどの構成を使用する場合は、もう少し作業が必要になります。
この短いチュートリアルでは、forの各操作にカウンターを含めることができるいくつかの方法を見ていきます。
2. カウンターの実装
簡単な例から始めましょう。 映画の順序付きリストを取得し、ランキングとともに出力します。
List<String> IMDB_TOP_MOVIES = Arrays.asList("The Shawshank Redemption",
"The Godfather", "The Godfather II", "The Dark Knight");
2.1. forループ
for loop はカウンターを使用して現在のアイテムを参照するため、リスト内のデータとそのインデックスの両方を簡単に操作できます。
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. for各ループ
映画のリストを引き続き使用しますが、各構成に対して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. それぞれの機能的な
必要なたびにcounter拡張機能を書き込むと、コードが重複し、counter変数をいつ更新するかに関する偶発的なバグが発生する可能性があります。 したがって、Javaの機能インターフェイスを使用して上記を一般化することができます。
まず、ループ内の動作を、コレクション内のアイテムとインデックスの両方のコンシューマーとして考える必要があります。 これは、 BiConsumer を使用してモデル化できます。これは、2つのパラメーターを受け取るaccept関数を定義します。
@FunctionalInterface
public interface BiConsumer<T, U> {
void accept(T t, U u);
}
ループの内部は2つの値を使用するものであるため、一般的なループ操作を記述できます。 foreachループが実行されるソースデータのIterable と、各アイテムとそのインデックスに対して実行する操作の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. Streamを使用してforEachにカウンターを追加する
Java Stream APIを使用すると、データがフィルターと変換をどのように通過するかを表現できます。 また、forEach関数も提供します。 それをカウンターを含む操作に変換してみましょう。
Stream forEach 関数は、コンシューマーを使用して次のアイテムを処理します。 ただし、その 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 に委任され、アルゴリズムがアイテムとそのインデックスの両方を処理できるようにします。
moviesと呼ばれるStreamに対する映画ランキングの例でこれが使用されていることを見てみましょう。
List rankings = new ArrayList<>();
movies.forEach(withCounter((i, movie) -> {
String ranking = (i + 1) + ": " + movie;
rankings.add(ranking);
}));
forEach の内部には、 withCounter 関数の呼び出しがあり、カウントを追跡し、forEachのConsumerとして機能するオブジェクトを作成します。 ]操作もその値を渡します。
5. 結論
この短い記事では、Java forの各操作にカウンターを接続する3つの方法について説明しました。
forループの各実装で現在のアイテムのインデックスを追跡する方法を見ました。 次に、このパターンを一般化する方法と、ストリーミング操作に追加する方法を確認しました。
いつものように、この記事のサンプルコードは、GitHubでから入手できます。