読者です 読者をやめる 読者になる 読者になる

Java8のforEachを使った繰り返し処理について

スポンサーリンク

Java8で追加されたforEachメソッドの使い方について簡単にまとめました。最初はfor文から全て置き換えて使えると思ってましたが、そうは上手くいかなかったようです。

forEachメソッドとは

forEachメソッドはコレクションや配列に対して繰り返し処理を行うためのメソッドです。今までだとfor文で記述していた部分をラムダ式で置き換える事ができるようになりました。

Java8では追加された機能の Stream API を使用してコレクションや配列の複数の値に対して様々な処理を行えます。使い方としてはコレクションや配列を一度Streamに変換して各メソッドを実行していくわけですが、forEach と spliterator メソッドに関しては Iterable インターフェースに default メソッドとして追加されていますので、Streamに変換する必要がありません。

ここで少し違和感がありますが、インターフェースに追加された default メソッドもJava8の新機能になります。メソッドの前にdefault void forEach( ~ )を記述することによってインターフェースでも実装を持つメソッドが定義できるようになったのです。

forEachの使い方

List・Set・配列・Mapでそれぞれ使い方を見てみたいと思います。

ListとSetの場合

 前回の記事でも少し触れましたが、forEachメソッドは関数型インターフェースのConsumer<? super T> actionを引数として渡す必要があります。

List<String> lists = Arrays.asList("hoge", "fuga", "bar");
Set<String> sets = new HashSet<>(lists);

lists.forEach(list -> System.out.println(list));   // ラムダ式使用時

sets.forEach(new Consumer<String>() {   // 参考までにラムダ式未使用時
    @Override
    public void accept(String str) {
        System.out.println(str);
    }
});

これが今までの拡張for文だと次のように書いてました。ラムダ式の場合は型推論をしてくれるのでfor文のように型を書く必要がありません。

for (String list : lists) {
    System.out.println(list);
}

配列の場合

配列に関しては一度Streamに変換する必要がありますが、Java8からArrays.streamという配列からStreamを生成してくれるメソッドが追加されています。

String[] arrays = {"hoge", "fuga", "bar"};

Arrays.stream(arrays).forEach(arr -> System.out.println(arr));

Mapの場合

MapはIterableインターフェースを継承していませんが、Mapインターフェース自身にdefaultメソッドとしてforEachが追加されました。関数型インターフェースのBiConsumer<? super K, ? super V> actionを渡す必要があり、引数も2つ必要です。

Map<String, String> maps = new HashMap<>();
...
maps.forEach((key, value) -> System.out.println(key + ":" + value));

forEachのソースを見てみると、やってることはentrySet()でfor文を回してるだけです。

for(Map.Entry<String, String> hoge : maps.entrySet()) {
    System.out.println(hoge.getKey() + ":" + hoge.getValue());
}

副作用について

forEachは戻り値を返さないので、副作用を起こすためのメソッドになります。 副作用を起こす処理とは、メソッドの外部に変化を起こすような処理のことです。

List<String> lists = Arrays.asList("hoge", "fuga", "bar");
Set<String> result = new HashSet<String>();   // 実質的finalな変数

// 値を追加してるだけなのでコンパイルエラーにならないが副作用が起きている
lists.forEach(list -> result.add(list));

// resultの参照を変えるのでコンパイルエラーになる
lists.forEach(list -> result = new HashSet<>();

実際にこのような処理はないと思いますが新しくコレクションを生成する場合などは、外部の変数を使うのではなくstream().map( ~ ).collect(Collectors.toSet());等を使って新しく生成する方がいいと思います。

continueとbreak

for文では当たり前に使ってた continue と break が効きません。ループ制御はできませんがreturnを使うとcontinueに似た動きをしてくれます。

lists.forEach(list -> {
    if (list.equals("fuga")) return;
    System.out.println(list);
});

匿名クラスで書くと分かりやすいです。

lists.forEach(new Consumer<String>() {
    @Override
    public void accept(String list) {
        if (list.equals("fuga")) return;
        System.out.println(list);
    }
});

参考記事

 Java8で最もインパクトのある構文拡張、デフォルトメソッド - きしだのはてな

 Java8 Stream APIの基本(2) - 中間操作の種類と並列処理、副作用 - エンタープライズギークス (Enterprise Geeks)

関連記事

 Java8ラムダ式の使い方の基本 - TASK NOTES

 Java8のStream APIの使い方(Streamの生成編)

 Java8のStream APIの使い方(中間操作編① - filter, map)

 Java8のStream APIの使い方(中間操作編② - flatMap, distinct, limit, skip)

 Java8のStream APIの使い方(中間操作編③ - sorted, peek)

 Java8のStream APIの使い方(終端操作編① - anyMatch, allMatch, noneMatch) - TASK NOTES