Java

Java MapのforEachをエレガントに書く方法とメリット

JavaのMapにはforEachというループの書き方が存在します。

ネットなどを見ると、普通のループと同じ感じで書いてあり、あまり凄さがわからないですが、実は使いこなすと非常にイケイケで便利なコードを書くことができます。

本記事ではエレガントにMapのforEachを書く方法をご紹介します。

よくあるMapのforEachの例

ネットでよく見る例が以下のような書き方です。

import java.util.HashMap;
import java.util.Map;

public class MapForEach {
    public static void main(String[] args) {
        
        Map<String, String> userMap = new HashMap<>();
        userMap.put("00111", "山田太郎");
        userMap.put("00112", "スズキ次郎");
        userMap.put("00113", "斎藤健二");
        // 一般的な例
        userMap.forEach((key, val) -> System.out.println("社員番号:" + key + " " + "氏名:" + val));

    }
}

実行結果は以下のとおりです。

社員番号:00112 氏名:スズキ次郎
社員番号:00113 氏名:斎藤健二
社員番号:00111 氏名:山田太郎

普通な感じですね。

なんとなく使い方はわかりますが、別にEntrySetのfor文で良いんじゃないか?という感じですよね。

MapのforEachは引数が最大のポイントです

これをいかに使いこなすことができるかで、forEachの輝きが変わってきます。

MapのforEachをエレガントに使用する方法

forEachの引数は「BiConsumer<T,U>」です。

つまり、あらかじめBiConsumerを宣言しておき、それを利用するようにすることが可能です。

また、BiConsumerはandThenメソッドを使用することで、複数呼び出すことができます。

これをうまく使えると、以下のようなメリットが出てきます。

  • 処理を分離して管理できる
  • 呼び出し方を誤らないようにできる

まずはサンプルソースを御覧ください。

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;

public class MapForEach {
    public static void main(String[] args) {
        
        Map<String, String> userMap = new HashMap<>();
        userMap.put("00111", "山田太郎");
        userMap.put("00112", "スズキ次郎");
        userMap.put("00113", "斎藤健二");

        // BiConsumerを宣言
        BiConsumer<String, String> logAccess = MapForEach::logAccess;
        BiConsumer<String, String> insertAccessRecord = MapForEach::insertAccessRecord;

        // BiConsumerで宣言した処理を呼び出し
        userMap.forEach(logAccess.andThen(insertAccessRecord));

    }

    static void logAccess(String employeeId, String name) {
        System.out.println("access! 社員番号:" + employeeId + " " + "氏名:" + name );
    }

    static void insertAccessRecord(String employeeId, String name) {
        System.out.println("アクセスログをデータベースに投入! 社員番号:" + employeeId + " " + "氏名:" + name );
    }

}

実行結果は以下のとおりです。

access! 社員番号:00112 氏名:スズキ次郎
アクセスログをデータベースに投入! 社員番号:00112 氏名:スズキ次郎
access! 社員番号:00113 氏名:斎藤健二
アクセスログをデータベースに投入! 社員番号:00113 氏名:斎藤健二
access! 社員番号:00111 氏名:山田太郎
アクセスログをデータベースに投入! 社員番号:00111 氏名:山田太郎

サンプルを元にメリットを解説します。

処理を分離して管理できる

要はループで実施したい処理をメソッドとして切り出しておくことで、処理を分離することができるということです。

ループ内に処理を書いてしまうと、処理が膨れてしまい可読性が落ちるなどのデメリットがあります。

別のメソッドに切り出すことで処理が膨れてしまうことを防ぐことができたり、様々な箇所から呼べたり、作業者を分けることもできます。

こちらはforEach以前より存在していた、EntrySetを使用したループでも同様のことができました。

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

public class MapForEach {
    public static void main(String[] args) {
        
        Map<String, String> userMap = new HashMap<>();
        userMap.put("00111", "山田太郎");
        userMap.put("00112", "スズキ次郎");
        userMap.put("00113", "斎藤健二");
        
        // EntrySetを使用したループ
        for (Entry<String, String> user : userMap.entrySet()) {
            logAccess(user.getKey(), user.getValue());
            insertAccessRecord(user.getKey(), user.getValue());
        }

    }

    static void logAccess(String employeeId, String name) {
        System.out.println("access! 社員番号:" + employeeId + " " + "氏名:" + name );
    }

    static void insertAccessRecord(String employeeId, String name) {
        System.out.println("アクセスログをデータベースに投入! 社員番号:" + employeeId + " " + "氏名:" + name );
    }

}

ただしEntrySetを使用した方法には、弱点があります。

記述が煩雑ということと、引数を誤る可能性があるということです。

以下は引数を間違えてしまったコードです。

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

public class MapForEach {
    public static void main(String[] args) {
        
        Map<String, String> userMap = new HashMap<>();
        userMap.put("00111", "山田太郎");
        userMap.put("00112", "スズキ次郎");
        userMap.put("00113", "斎藤健二");
        
        // EntrySetを使用したループ
        for (Entry<String, String> user : userMap.entrySet()) {
            // 引数の順番を間違えた!
            logAccess(user.getValue(), user.getKey());
            insertAccessRecord(user.getKey(), user.getValue());
        }

    }

    static void logAccess(String employeeId, String name) {
        System.out.println("access! 社員番号:" + employeeId + " " + "氏名:" + name );
    }

    static void insertAccessRecord(String employeeId, String name) {
        System.out.println("アクセスログをデータベースに投入! 社員番号:" + employeeId + " " + "氏名:" + name );
    }

}

すると結果は以下のようになってしまいます。

access! 社員番号:スズキ次郎 氏名:00112
アクセスログをデータベースに投入! 社員番号:00112 氏名:スズキ次郎
access! 社員番号:斎藤健二 氏名:00113
アクセスログをデータベースに投入! 社員番号:00113 氏名:斎藤健二
access! 社員番号:山田太郎 氏名:00111
アクセスログをデータベースに投入! 社員番号:00111 氏名:山田太郎

社員番号と氏名が入れ替わってしまいました。

この弱点はforEachをうまく使うと防ぐことができます

呼び出し方を誤らないようにできる

EntrySetを使用したループではメソッドを呼び出す際に、引数を間違ってしまう可能性がありました。

しかし、forEachをうまく使うと防ぐことができます。

以下は初めのサンプルの抜粋です。

// BiConsumerを宣言
BiConsumer<String, String> logAccess = MapForEach::logAccess;
BiConsumer<String, String> insertAccessRecord = MapForEach::insertAccessRecord;

// BiConsumerで宣言した処理を呼び出し
userMap.forEach(logAccess.andThen(insertAccessRecord));

forEachの引数に設定することができる「BiConsumer<T,U>」にて、メソッド参照を使用しています。

これにより、BiConsumerのT,Uに設定される順番で引数がlogAccessやinsertAccessRecordに設定されます。

さて、T,Uに設定される順番が何かというと、MapのforEachの引数に書いてあるとおり、key, valueの順番で渡されます。

forEach(BiConsumer<? super K,? super V> action)

https://docs.oracle.com/javase/jp/8/docs/api/java/util/Map.html

そのため、自分で値を設定しなくて済むので、引数の順番を間違ってしまうことはなくなります。

人間はいくら注意しても間違えてしまうので、うまく使えれば品質を高めることができます。

まとめ

JavaのMapで使用できるループは色々あるが、forEachをうまく使いこなすことができれば、非常にエレガントなコードを書くことができます

エレガント以外にも重要なメリットはありますが、やはり書いていて気持ちがいいというのが一番重要ですよね(重要)。

是非、EntrySetだけではなく、forEachの使用を検討してみてください。

【お知らせ 無料!】未経験エンジニアがJavaでWebサイトを作成できるようになるための学習ロードマップを、無料で公開しています!

実体験に基づいて作成されているので、プログラミングスクールなどで指導されるロードマップにも劣らない品質です。

こちらの「【Java】エンジニア未経験者がJavaを効率的に勉強する手順を紹介します」リンクから無料で閲覧できるので、是非ご覧ください!

【2021/6 更新】エンジニア未経験者がJavaを効率的に勉強する手順を紹介しますプログラミングを勉強する際に候補に出てくる言語に Java があります。 最近は Java 以外の言語である、Ruby や Pyt...