Help us understand the problem. What is going on with this article?

[Java] Map#mergeが分かりづらい。

More than 1 year has passed since last update.

いまさらながら

先日Java 9がリリースされたにもかかわらず敢えてこのタイミングでJava 8を勉強し始めました。
周回遅れもいいところなのですが、今更ながらJava 8の新機能はステキすぎて感動してしまいます。
Java 8の新機能についての解説はググると分かりやすい記事がいくつも見つかりますので、全体の解説はそれらに譲るとして、ここでは私が分かりづらかった&あまり細かく解説している記事がないMap#mergeの動きについてメモしておきます。

Map#merge

「Map内にキー"key1"が存在していれば値を"CCC"で上書き、存在してなければ新しいキーとして値"BBB"を追加する」という処理を、Java 7までとJava 8での書き方を比べてみます。

Java7まで("key1"が存在するケース)
Map<String, String> map = new HashMap<String, String>();
map.put("key1", "AAA");

System.out.println(map);

if (map.containsKey("key1")) {
    map.put("key1", "CCC");
} else {
    map.put("key1", "BBB");
}

System.out.println(map);
実行結果
{key1=AAA}
{key1=CCC}
Java7まで("key1"が存在しないケース)
Map<String, String> map = new HashMap<String, String>();

System.out.println(map);

if (map.containsKey("key1")) {
    map.put("key1", "CCC");
} else {
    map.put("key1", "BBB");
}

System.out.println(map);
実行結果
{}
{key1=BBB}

Java 8で追加になったMap#mergeを使ってみると以下のようになります。
Mapに第1引数に指定したキー("key1")が存在しない場合は第2引数の値("BBB")がそのキー("key1")でputされ、キーが存在する場合は第3引数のremapping関数が実行されます。

Java8("key1"が存在するケース)
Map<String, String> map = new HashMap<String, String>();
map.put("key1", "AAA");

System.out.println(map);

map.merge("key1", "BBB", 
    (v1, v2) -> {
        return "CCC";
    });

System.out.println(map);
実行結果
{key1=AAA}
{key1=CCC}
Java8("key1"が存在しないケース)
Map<String, String> map = new HashMap<String, String>();

System.out.println(map);

map.merge("key1", "BBB", 
    (v1, v2) -> {
        return "CCC";
    });

System.out.println(map);
実行結果
{}
{key1=BBB}

2つの引数は?

ここで疑問に思ったのが、Mapにキーが存在するときに呼ばれるremapping関数に渡される2つの引数は何?ということです。上の例のv1v2ですが、全く使っていません。

ということでとりあえず何が渡されているのか出力してみます。

Java8("key1"が存在するケース)
Map<String, String> map = new HashMap<String, String>();
map.put("key1", "AAA");

System.out.println(map);

map.merge("key1", "BBB", 
    (v1, v2) -> {
        Sytem.out.printf("v1:%s, v2:%s\n", v1, v2);
        return "CCC";
    });

System.out.println(map);
実行結果
{key1=AAA}
v1:AAA, v2:BBB
{key1=CCC}

となりました。
つまりキー"key1"が存在していたとき、既に存在する値(例では"AAA")と存在しない場合に設定する値(例では"BBB")がこの関数に渡されます。
ただ「キーが存在しない場合に設定する値」が引数で渡されたとしても、キーが存在していた時に使われるケースは少ないのではないかと思うのです。
この値が渡されることで役に立ちそうなのは、例えばこの値を既存の値(文字列)に連結するときなど限られた用途になるのかもしれません。今回のような単純な置き換えの場合はJava 7までの書き方の方がむしろ分かりやすいですね。

ちなみに単純な連結であれば以下のようにremapping関数にString::concatを指定することで実現できます。

単純な連結はこれでOK!
Map<String, String> map = new HashMap<String, String>();

map.put("key1", "AAA");

System.out.println(map);

map.merge("key1", "BBB", String::concat);

System.out.println(map);
実行結果
{key1=AAA}
{key1=AAABBB}

なぜmergeを調べたのか

今回Map#mergeの動きを詳しく調べたのは、Map<String, List<String>>の操作でキーが存在する場合にListに値を追加するということをJava 8の新機能でスマートにできないかと考えたことがきっかけでした。具体的には以下のような感じで実装しました。

Java8
Map<String, List<String>> map = new HashMap<String, List<String>>();

map.put("key1", new ArrayList<String>(Arrays.asList("AAA")));
System.out.println(map);

// key1が存在すれば"CCC"をListに追加、存在しなければ"BBB"という要素を持ったListを追加
map.merge("key1", new ArrayList<String>(Arrays.asList("BBB")),
        (v1, v2) -> {
    v1.add("CCC");
    return v1;
});

System.out.println(map);
実行結果
{key1=[AAA]}
{key1=[AAA, CCC]}

Java 7以前だと以下のような感じです。

Java7まで
Map<String, List<String>> map = new HashMap<String, List<String>>();

map.put("key1", new ArrayList<String>(Arrays.asList("AAA")));
System.out.println(map);

if (map.containsKey("key1")) {
    // key1が存在すれば"CCC"をListに追加
    map.get("key1").add("CCC");
} else {
    // key1が存在しなければ"BBB"という要素を持ったListを追加
    map.put("key1", new ArrayList<String>(Arrays.asList("BBB")));
}

System.out.println(map);
実行結果
{key1=[AAA]}
{key1=[AAA, CCC]}

この例だとどちらも大して変わらないですね(;^ω^)
ただJava 8だとif-else文を無くせるので気持ちスッキリしました。

ということでMap#mergeが活躍するのは、「既にMap内に存在する値に対して何か変更を加えるとき」や「コードをスッキリさせたいとき」でしょうか。後者はこのメソッドに限らずラムダ式やStreamを使う理由でもありますが。

おまけ

余談ですがremapping関数でnullを返せばそのキーは削除されます。

キーを消してみる
Map<String, String> map = new HashMap<String, String>();
map.put("key1", "AAA");

System.out.println(map);

map.merge("key1", "BBB", 
    (v1, v2) -> {
        return null;
    });

System.out.println(map);
実行結果
{key1=AAA}
{}

もう少しJava 8で遊んでみることにします^^

naonaka
最近慣れ親しんだJavaを離れてJavaScriptで遊んでいる中年のIT系ソフトウェアエンジニア兼マネージャー。UXデザインやコピーライティングにも興味があるため、アプリのデザインやプロモーションのやり方にはやたらと拘りメンバーから面倒臭がられる存在。 好きなデザインパターンはTemplateMethodとStrategy。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away