Java foreachとは?Javaのforeach関連構文・機能を紹介
プログラム上でモノの集まり(配列、コレクション等)の各要素へ繰り返し処理を行う構文や機能を、一般にforeach文と言います。
Javaでforeach文に直接対応するものは拡張for文です。
ただ、foreach文を「何かへの繰り返し」という少し高い視点で見れば、Javaには拡張for文以外にも様々な手段があります。
大きな分類ではプログラミング言語としての構文と、ライブラリで提供される機能となります。
この記事では、Javaにおける広い意味でのforeach文と、実務のプログラミングで良く使う機能を、Javaの初心者向けに概観します。それぞれの構文の特徴を学んで、適材適所で使えるようになりましょう!!
目次
1.Javaの組み込み構文によるforeach
Javaでのループの基本はfor文とwhile文です。さらに、Javaのfor文には拡張for文というものが存在します。
一般的にJavaでのforeach文と言うと、この拡張for文を指すのが普通と思われますので、拡張for文・for文/while文の順番でforeachする時のサンプルを紹介します。
1-1.拡張for文
1-1-1.拡張for文の基本
Java 1.5で追加された拡張for文によるforeachを用いると、配列やコレクションの全要素へ先頭から簡単にアクセスできます。しかも、通常のfor文で必要な、インデックスや配列・コレクションなどからの要素の取り出し処理を自分で書かなくてもいいのです。
ちなみに、拡張for文は、通常のfor文で行っていることを簡略化して書けるようになっただけです。つまり、for文でやるような処理をJavaが裏で勝手にやってくれているだけです。これを糖衣構文(sugar syntax)と言ったりします。
// 拡張for文の構文 for (取り出す要素の変数宣言 : 配列あるいはjava.lang.Iterableのインスタンス) { 繰り返し処理; }
なお、通常のfor文では()の中での区切りが“;”(セミコロン)二つですが、拡張for文では“:”(コロン)一つだけなので間違えないようにしましょう。
拡張for文の例としては以下のとおりです。配列とjava.util.Listを用いてみました。
// 配列での例 int[] array = {1, 2, 3}; for (int e : array) { // eはElement(要素)のEです。以下のサンプルで全て同じです。 System.out.println(e); // 1, 2, 3と順番に出力される }
// Iterableでの例(ここではList) List<Integer> list = Arrays.asList(1, 2, 3); for (Integer e : list) { System.out.println(e); // 1, 2, 3と順番に出力される }
for文と比較した場合の拡張for文の制限は、必ず配列・コレクションの先頭からの処理となることです。後ろから処理をしたい場合は、拡張for文では直接対応できませんので、あらかじめ逆順にソートし直すなどの処置が必要です。一つ飛ばしなどにしたい場合も、ひと工夫必要です。
1-1-2.Iterableとは繰り返しできるモノ
先程、java.lang.Iterableというものが出てきました。Iterableとは「繰り返し処理できるもの」を表現しているインターフェイスです。代表的なIterableはListやSetなどのコレクションですが、逆に言えば拡張for文の対象とできるのは、ListやSetでなければならない、というわけではないのですね。
つまり、Iterableを正しく実装しているクラスなら何でも、拡張for文でforeachできるのです。すなわち、以下のようにも書けるということです。
Iterable<Integer> list = Arrays.asList(1, 2, 3); // 変数listの型をListではなくてIterableとしてみた for (Integer e : list) { // Iterableであれば拡張for文の対象とできる System.out.println(e); }
なお、Javaの標準APIを見てみると、非常に多くのクラスやインターフェイスがこのIterableを実装・継承しています。意外なものがIterableだったりするので(java.sql.SQLExceptionなど!!)、APIを良く見てみると面白いかもしれません。
1-1-3.【参考】拡張for文での多次元配列やネストしたコレクションの扱い方
さて、拡張for文で取り出す変数の型は、配列なら配列で保持している要素の型です。
ということは、多次元配列を拡張for文で扱う場合の変数がどうなるかというと、二次元配列なら「一次元目の配列の要素は、二次元目の配列への参照を保持している」のですから、二次元目の配列自体が取り出す変数となるのです。
// 二次元配列での拡張for文の例 int[][] array = {{1, 2, 3}, {4, 5, 6}}; for (int[] subarray : array) { // 一次元目の配列から二次元目の配列への参照を取り出して、 for (int e : subarray) { // さらに二次元目の配列でforeachすると、 System.out.println(e); // 1, 2, 3, 4, 5, 6と順番に出力される } }
Listの例では、List内に保持している型をジェネリクスの型変数(List<Integer>のInteger)で指定しています。ですので、拡張for文での変数の型もIntegerとなります。
配列と同じように、保持している型を順に取り出すのですから、例えばList内にさらにListを保持しているのなら、以下のようになります。
// Listの中に、さらにListを保持している場合の拡張for文の例 List<List<Integer>> list = new ArrayList<>(); // Listの中には、List<Integer>を保持している list.add(Arrays.asList(1, 2, 3)); list.add(Arrays.asList(4, 5, 6)); for (List<Integer> sublist : list) { // ListからList<Integer>を取り出して、 for (Integer e : sublist) { // 取り出したList<Integer>のIntegerでforeachすると、 System.out.println(e); // 1, 2, 3, 4, 5, 6と順番に出力される } }
ちなみに、Listの場合で型変数での保持する型を指定していないなら、取り出す変数の型はObjectとなります。Objectは全てのクラスのスーパークラスですから、どんな型でも無難に代入できるからですね。
ですが、Objectの場合はキャストが必要なことがほとんどですので、実体として何を格納しているか忘れないようにしましょう。そうしないとClassCastExceptionが発生してしまいます。
// Listに型変数を指定しない場合はObjectになる List list = Arrays.asList(1, 2, 3); for (Object e : list) { Integer i = (Integer)e; // 入っている値はIntegerのはずなので、キャストする System.out.println(i); // 1, 2, 3と順番に出力される }
1-2.for文/while文
for文によるforeachでは、一般的には配列・コレクションにアクセスするためのインデックスを変数で増減させて、順番にアクセスしていきます。例としては以下のとおりで、良くあるものですね。
int[] array = {1, 2, 3}; for (int i = 0; i < array.length; i++) { int e = array[i]; System.out.println(e); }
List<Integer> list = Arrays.asList(1, 2, 3); for (int i = 0; i < list.size(); i++) { Integer e = list.get(i); System.out.println(e); }
while文でforeachをする場合、while文自体には配列やコレクションの要素を取り出す機能はないので、添え字や特定位置の要素を取り出すメソッド、または後述するIteratorを利用したループとなります。
int[] array = {1, 2, 3}; int i = 0; while (i < array.length) { int e = array[i]; System.out.println(e); i++; }
List<Integer> list = Arrays.asList(1, 2, 3); int i = 0; while (i < list.size()) { Integer e = list.get(i); System.out.println(e); i++; }
このように、本質的には行っていることはfor文と変わりありません。インデックスに使っている変数の宣言場所と、加算をしている場所が違うだけです。
1-3.for文/while文のデメリットはインデックス管理
for文/while文のデメリットはインデックス管理です。変数を使ってインデックス管理をすること自体が、プログラム上のミスに繋がる可能性を生むのです。
ここまでの例のとおり、配列やコレクションへはインデックスを使ってアクセスします。それらのインデックスはintなどの変数で表現し、ループ内で数値を増減させます。
インデックスを変数で表現すると、例えば要素へのアクセスを1個飛ばしにできますし、先読みもできるので、ループ処理を柔軟に作れます。ですから、for文/while文を使うことはこれからもなくならないでしょう。
しかし、配列・コレクションのインデックスを超えたアクセスをJavaのコンパイラはチェックしませんので(実行時にはチェックします)、for文/while文を使ったプログラムが正しく動くかどうかは、実際に動かさないと分かりません。それに、実際にはプログラマーの技量に大きく依存してしまうのが実状です。
2.ライブラリによるforeach(Java 7まで)
ここからは純粋なJavaの構文ではなく、ライブラリが提供する機能を組み合わせたforeach文になります。
まだfor文やwhile文を記述する必要がありますが、配列やコレクションのインデックスを意識せずにforeachできるのが、大きな改善点です。
2-1.Iterator
Java 1.2で追加されたのがjava.util.Iteratorです。コレクションの内容を順番に全て取り出して処理を実行したい時に使います。IteratorはJava 1.2でJava Collections Frameworkが整備された際に追加されました。
// Iteratorの抽象メソッド boolean hasNext() 反復処理でさらに要素がある場合にtrueを返します。 E next() 反復処理で次の要素を返します。(※Eはジェネリクスの型変数です) void remove() ベースとなるコレクションから、このイテレータによって最後に返された要素を削除します(オプションの操作)。
// while文を使う場合 List<Integer> list = Arrays.asList(1, 2, 3); Iterator<Integer> it = list.iterator(); // ListからIteratorを取得する while (it.hasNext()) { // Iteratorに「次」の要素があるか確認し、 Integer e = it.next(); // Iteratorから取り出す System.out.println(e); }
// for文を使う場合 List<Integer> list = Arrays.asList(1, 2, 3); for (Iterator<Integer> it = list.iterator(); it.hasNext();) { // Iteratorの取り出しと、hasNextを1行で書く Integer e = it.next(); System.out.println(e); }
ListやSetのようなコレクションなどからIteratorを生成し、次の要素があるか問い合わせ(Iterator.hasNext())、あるなら取り出す(Iterator.next())…という操作を続けます。ですので、Iteratorを使うならfor文やwhile文との組み合わせが必須となります(どちらを使うかはお好みで)。
プログラムを見て頂ければわかるとおり、コレクションからのインデックスによる要素の取出しが不要です。これにより、今どのインデックスの要素を処理しているということを意識しなくても良いのです。
Iteratorを返すメソッドを持つクラスは数多くありますので、標準APIないし様々なフレームワークのAPIドキュメントをよく読んでみると、意外な発見があるかもしれませんね。慣習的にはiterator()というメソッド名が多いです。
ちなみに、Iteratorでは要素の削除(Iterator.remove())も行えます。これは拡張for文では簡単にはできないことです。ループ中に削除判断ができ、さらにコレクション操作時のカウンタ操作が不要なので、とても使いやすいですね。
List<Integer> list = Arrays.asList(1, 2, 3); Iterator<Integer> it = list.iterator(); while (it.hasNext()) { Integer e = it.next(); System.out.println(e); if (e == 2) { // 値が2ならListから削除する it.remove(); } } System.out.println(list); // → [1, 3] 2だけが削除された
2-2.Iteratorのデメリットはプログラムを書く部分がまだ多いこと
便利なIteratorですが、デメリットはループ構文がまだ事実上必須なところと、自分で要素を取り出さなければならないところです。さらに、それらを書かなくてもプログラムはコンパイルできるので、相変わらず実行してみなければ正しく動くか分からないという点です。
つまり、Iteratorが拡張for文に劣るのは、Iterator.hasNext()やIterator.next()をきちんと呼び出せているか、プログラマーが常に意識しなければならないところです。ただ、ループの制御という面では拡張for文よりは柔軟に書ける場合がありますので、適材適所という感じです。
しかし、プログラムを作っている側からすると「単にコレクションでforeachしたいだけなのに注意しなければならないことが多いのは面倒くさいなぁ」という印象はまだ残ります。
2-3.【参考】Enumeration
java.util.Enumerationというものもあります。これはJava 1.0の時代から存在していました。IteratorはEnumerationの代わりに導入されたものです。使い方は以下のとおりです。
Vector<Integer> vector = new Vector<>(); vector.add(1); vector.add(2); vector.add(3); Enumeration<Integer> enumeration = vector.elements(); while (enumeration.hasMoreElements()) { Integer e = enumeration.nextElement(); System.out.println(e); }
Vector<Integer> vector = new Vector<>(); vector.add(1); vector.add(2); vector.add(3); for (Enumeration<Integer> enumeration = vector.elements(); enumeration.hasMoreElements(); ) { Integer e = enumeration.nextElement(); System.out.println(e); }
Iteratorと同様に、コレクションから呼び出したEnumerationに、次の要素があるか問い合わせ(Enumeration.hasMoreElements())、あるなら取り出す(Enumeration.nextElement())…という操作を続けます。ですので、これまたfor文やwhile文との組み合わせが必須となります。
使い方自体はIteratorとほぼ同じですが、元となるコレクションへの削除操作(remove)がないのと、メソッド名が少し長いのが違いです。今はEnumerationは標準API内では新しく使われることはありませんので、同じことをやりたいならIteratorを使うのが普通です。
なお、EnumerationからIteratorを生成するメソッド(Enumeraiton.asIterator())がJava 9から追加されましたので、使ってみても良いでしょうね。
Vector<Integer> vector = new Vector<>(); vector.add(1); vector.add(2); vector.add(3); Enumeration<Integer> enumeration = vector.elements(); Iterator<Integer> it = enumeration.asIterator(); while (it.hasNext()) { Integer e = it.next(); System.out.println(e); }
3.ライブラリによるforeach(Java 8以降)
Java 8で、ループ構文を使わずにコレクションや配列の各要素へ処理できる構文とライブラリが追加・整備されました。具体的には、関数型インターフェイス、ラムダ式、メソッド参照、そしてStream APIです。
Stream APIの大きな特徴は、そもそもの目的がforeach的な処理を行うためのAPIだということです。Stream APIの概念や中間処理・終端処理などに関してはここでは詳細は述べませんので、別途調べてみてください。面白いですよ。
ここでは、foreach文的に使えるStream APIの終端処理の一つ「Stream.forEach(Consumer)」を中心に、「Iterable.forEach(Consumer)」と「Iterator.forEachRemaining(Consumer)」を紹介します。
3-1.Stream.forEach(Consumer)
3-1-1.Stream.forEach(Consumer)の例
ListなどのStreamを生成できるクラスからStreamを生成してforEach()メソッドを呼び出すと、Stream内にある全要素にforEach()の引数としたConsumerによる処理が適用されます。
// Stream.forEach()のメソッドシグネチャ、いずれもConsumerを引数に取る void forEach(Consumer<? super T> action); void forEachOrdered(Consumer<? super T> action);
Stream.forEach()の引数であるConsumerは、以下のように1つの抽象メソッドのみ定義された「関数型インターフェイス」です。Consumerは、受け取った引数への何かの「処理」を表現しているのです。
public interface Consumer<T> { void accept(T t); }
Stream.forEach(Consumer)の実行例は以下のとおりです。この例ではConsumerをラムダ式・メソッド参照・通常のクラスの3パターンで生成しています。ラムダ式とメソッド参照はぱっと見では分かりづらいですが、結局はConsumerを実装したクラスを簡易的に生成するための糖衣構文に過ぎません。
List<Integer> list = Arrays.asList(1, 2, 3); list.stream().forEach(e -> System.out.println(e)); // 1, 2, 3が出力される(ラムダ式でConsumerのインスタンスを生成) list.stream().forEach(System.out::println); // 1, 2, 3が出力される(メソッド参照でConsumerのインスタンスを生成) // 分かりやすくするために、ラムダ式・メソッド参照を使わない書き方もしてみます。 // 上の例と実質的には同じことをしています。 class SampleConsumer implements Consumer<Integer> { void accept(Integer e) { System.out.println(e); } } Consumer<Integer> c = new SampleConsumer(); list.stream().forEach(c);
見てわかるとおり、for文/拡張for文/while文/Iteratorに比べて、プログラムが格段に短くなりました。元々は複数行が必要なところを1行で書けています。それに、1番目のforEachは変数eの型を(見た目上は)宣言しておらず、2番目のforEachに至っては要素から取り出す変数の宣言すら行っていません。
3-1-2.Stream APIの特徴はループ構文が不要なこと
Stream.forEach(Consumer)では、今までのfor文/拡張for文/while文/Iteratorで必要だったループ構文が不要です。
Steam APIでは「処理」を関数型インターフェイスのインスタンスとして生成し、Stream内の各要素に処理を「適用」します。従来の方法では繰り返しのループ構文が先にあり、その中で繰り返す要素を「取り出して」処理をします。
つまりStream APIは、従来のループ構文やIteratorと繰り返しの考え方そのものが違っているのです。これを正しく理解しないと、痛い目を見るかもしれません。
なお、for文/while文ではできていたcontinueやbreakがStream APIではできません。returnでメソッドを終了するのはcontinueに比較的近い意味になりますが、breakの代わりは存在しません。ですので、従来の構文との使い分けが必要です。
List<Integer> list = Arrays.asList(1, 2, 3); list.stream().forEach(e -> { if (e == 2) { return; // continueの代わりにreturnしてメソッドの実行を終わらせる } System.out.println(e); });
それに、for文/while文ではできる、1つ前・1つ後などの他要素の参照ができません。あくまで一つ一つの要素へ処理を適用するのみです。これもStream APIの大きな制約なので、全てをStream APIで実装することはできないことは覚えておきましょう。
3-2.Iterable.forEach(Consumer)
Java 8では、Iterable単体でも各要素への処理ができるメソッド「Iterable.forEach(Consumer)」が追加されました。これを使えば、Iterator自体の出番が大きく減りそうです。
List<Integer> list = Arrays.asList(1, 2, 3); // ListはIterableを継承しているインターフェイス!! list.forEach(e -> System.out.println(e)); // → 1, 2, 3
これだとIteratorではループさせなければならない処理を1行で書けています。繰り返し実行したい処理の部分だけをConsumerで書けばいいのです。Streamによる中間処理が不要なのであれば、これを使うのが簡単ですね。
3-3.ループ構文から関数型インターフェイスの活用へ
この章で紹介したものは、関数型インターフェイスとラムダ式を活用していました。これはJavaにおける繰り返し処理の考え方が変わってきていることを意味しています。
前述のとおり、for/while/Iterator(旧構文)の場合、インデックス管理や次要素の有無の確認、ループ内での参照用変数への代入などが必要です。それらは、繰り返したい処理自体には直接関係しないけれども、書かなければ動かせないから書いているものです。つまり、いわゆるボイラープレートです。
プログラムのバグが発生する箇所は、むろん処理本体もですが、ボイラープレート部分であることも多いものです。配列などを参照するためのインデックス管理をミスするのがその最たるものです。例えば多次元配列を扱う際のインデックス管理は結構難しいですよね。
プログラムを書けば必ずミスをします。同じことができるのであれば、プログラムを書く量そのものが少ないほどミスをする可能性が下がります。最近のプログラミング言語のトレンドは、そのようなミスを生む可能性がある記述を極力排除することと、ミスをそもそも起こせないプログラム構文の採用です。
さらに、Consumerなどの関数型インターフェイスの導入目的は、処理したいことを細かく分けて記述するスタイルのプログラミングをするためです。大きく長いループ内でたくさんの処理を一度に書くのではなく、再利用できる小さな処理をたくさん繋ぎ合わせるのが、最近のプログラミングの潮流なのです。
3-4.【参考】Iterator.forEachRemaining(Consumer)
前述のとおり、Iteratorを使う際はループ構文とセットになるのがJava 7までの常識でした。ですが、Java 8でループ不要でコレクションへの要素ごとに処理を実行できるメソッド「Iterator.forEachRemaining(Consumer)」が追加されています。
使い方は前述したIteratable.forEach(Consumer)と同じです。
List<Integer> list = Arrays.asList(1, 2, 3); Iterator it = list.iterator(); it.forEachRemaining(e -> System.out.println(e)); // → 1, 2, 3
Iteartorからの要素取得処理を書かなくてもいいので、プログラミング上でのミスも減ることが期待できます。
なお、メソッド名に“Remaining”と付いていることからも推測できるかもしれませんが、通常のIteratorでの繰り返し処理の途中から実行することもできます。例えば以下のとおりです。
List<Integer> list = Arrays.asList(1, 2, 3); Iterator it = list.iterator(); System.out.println(it.next()); // → 1 it.forEachRemaining(e -> System.out.println(e)); // → 2, 3
4.再帰呼び出しによるforeach
最後に、繰り返し構文・Iterator・Stream APIを使わないforeachのやり方を紹介します
import java.util.Arrays; import java.util.List; import java.util.function.Consumer; class RecursiveCallForeach { <T> void foreach(List<T> list, Consumer<T> c) { if (list.isEmpty()) { return; } c.accept(list.get(0)); foreach(list.subList(1, list.size()), c); } public static void main(String[] args) { RecursiveCallForeach rcf = new RecursiveCallForeach(); List<Integer> list = Arrays.asList(1, 2, 3); rcf.foreach(list, e -> System.out.println(e)); } }
メソッドforeachでは常にListの先頭要素にだけ何かの処理を行い、Listの残り部分から新しいListを生成してforeachを再度呼び出します。これを繰り返す内に、引数のListの内容がなくなったら処理終了です。ステップをじっくり追いながら読んでみてください。
実行ステップのイメージ
foreach [1, 2, 3]
┗foreach [2, 3]
┗foreach [3]
┗foreach [] → return!
これがいわゆる「再帰呼び出し」です。世の中は広いもので、繰り返し構文を持たないプログラミング言語が数多く存在します。その代表格が関数型プログラミング言語です。そのような言語では繰り返しを再帰呼び出しで実装することが普通です。ここでは、それをJava風に実装してみたのです。
メリットはシンプルさです。Listへの事実上の操作は先頭要素の取出しと後続Listの切り出しだけで、プログラムミスに繋がるインデックス操作(加算・減算等)をしていません。後は、メソッド実行の副作用がない(かもしれない)ことが挙げられますが、詳細は述べませんので、興味があれば調べてみてください。
そして、再帰呼び出しとは直接関係しませんが、実行したい処理をメソッド内に記述していないのもポイントです。処理自体はConsumerなのでいくらでも差し替えが効きます。関数型プログラミング言語では「処理」を関数オブジェクトで表現し、自由に差し替えられます。Javaもその境地に一歩近付いたのです。
デメリットは分かりづらさです。Javaでは再帰をあまり使わない人もいるでしょうから、面喰らった方も多いでしょう。それに、普通のループ構文に比べればメモリも使います。foreachを再度呼び出す度に、Listのインスタンスを新しく生成するからです。
5.まとめ
この記事ではJavaでのforeach文について概観しました。foreachのやり方は、Javaのプログラミング言語としての構文や、ライブラリの機能、再帰呼び出しなど色々とありますし、それぞれに長短があります。
それに、この記事では紹介しませんでしたが、Javaの標準APIには様々なforeach的に使えるクラスがあります。例えば、ディレクトリ探索をforeach的にできるFileVisitorやFiles.walk()などです。こうできたらいいな、という機能はもう標準API内に既にあったりしますので、調べてみても面白いでしょう。
foreach的な処理をする時に、用途に応じたものを自在に選べるようになって、楽しく効率的にプログラミングできるようになりましょう!!
コメント