2つのマップとJava 8のマージ
1. 前書き
このクイックチュートリアルでは、we’ll demonstrate how to merge two maps using the Java 8 capabilities。
具体的には、エントリが重複しているマップなど、さまざまなマージシナリオを検討します。
2. 初期化
まず、2つのMapインスタンスを定義しましょう。
private static Map map1 = new HashMap<>();
private static Map map2 = new HashMap<>();
Employeeクラスは次のようになります。
public class Employee {
private Long id;
private String name;
// constructor, getters, setters
}
次に、いくつかのデータをMapインスタンスにプッシュできます。
Employee employee1 = new Employee(1L, "Henry");
map1.put(employee1.getName(), employee1);
Employee employee2 = new Employee(22L, "Annie");
map1.put(employee2.getName(), employee2);
Employee employee3 = new Employee(8L, "John");
map1.put(employee3.getName(), employee3);
Employee employee4 = new Employee(2L, "George");
map2.put(employee4.getName(), employee4);
Employee employee5 = new Employee(3L, "Henry");
map2.put(employee5.getName(), employee5);
マップ内のemployee1エントリとemployee5エントリには同じキーがあり、後で使用することに注意してください。
3. Map.merge()
Java 8 adds a new merge() function into the java.util.Map interface。
merge()関数のしくみは次のとおりです。指定されたキーがまだ値に関連付けられていないか、値がnullの場合、キーは指定された値に関連付けられます。
それ以外の場合は、指定された再マッピング関数の結果で値を置き換えます。 再マッピング関数の結果がnullの場合、結果が削除されます。
まず、map1からすべてのエントリをコピーして、新しいHashMapを作成しましょう。
Map map3 = new HashMap<>(map1);
次に、マージルールとともにmerge()関数を紹介しましょう。
map3.merge(key, value, (v1, v2) -> new Employee(v1.getId(),v2.getName())
最後に、map2を繰り返し処理し、エントリをmap3にマージします。
map2.forEach(
(key, value) -> map3.merge(key, value, (v1, v2) -> new Employee(v1.getId(),v2.getName())));
プログラムを実行して、map3の内容を出力してみましょう。
John=Employee{id=8, name='John'}
Annie=Employee{id=22, name='Annie'}
George=Employee{id=2, name='George'}
Henry=Employee{id=1, name='Henry'}
その結果、our combined Map has all the elements of the previous HashMap entries. Entries with duplicate keys have been merged into one entry。
また、最後のエントリのEmployeeオブジェクトにはmap1からのidがあり、値はmap2から選択されていることがわかります。
これは、マージ関数で定義したルールのためです。
(v1, v2) -> new Employee(v1.getId(), v2.getName())
4. Stream.concat()
Java 8のStream APIも、問題の簡単な解決策を提供できます。 まず、we need to combine our Map instances into one Stream。 これはまさにStream.concat()操作が行うことです。
Stream combined = Stream.concat(map1.entrySet().stream(), map2.entrySet().stream());
ここでは、パラメータとしてマップエントリセットを渡します。 Next, we need to collect our result into a new Map。 そのために、Collectors.toMap()を使用できます。
Map result = combined.collect(
Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
その結果、コレクターはマップの既存のキーと値を使用します。 しかし、このソリューションは完璧とはほど遠いものです。 コレクターが重複するキーを持つエントリに遭遇するとすぐに、IllegalStateExceptionをスローします。
この問題を処理するには、3番目の「マージ」ラムダパラメーターをコレクターに追加するだけです。
(value1, value2) -> new Employee(value2.getId(), value1.getName())
重複キーが検出されるたびにラムダ式を使用します。
最後に、すべてをまとめる:
Map result = Stream.concat(map1.entrySet().stream(), map2.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(value1, value2) -> new Employee(value2.getId(), value1.getName())));
最後に、コードを実行して結果を確認しましょう。
George=Employee{id=2, name='George'}
John=Employee{id=8, name='John'}
Annie=Employee{id=22, name='Annie'}
Henry=Employee{id=3, name='Henry'}
ご覧のとおり、キー“Henry”の重複エントリは、the id of the new Employee was picked from the map2 and the value from map1の新しいキーと値のペアにマージされました。
5. Stream.of()
Stream APIを引き続き使用するために、Stream.of()を使用してMapインスタンスを統合ストリームに変換できます。
ここでは、ストリームを操作するために追加のコレクションを作成する必要はありません。
Map map3 = Stream.of(map1, map2)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> new Employee(v1.getId(), v2.getName())));
まず、we transform map1 and map2 into a single stream。 次に、ストリームをマップに変換します。 ご覧のとおり、toMap()の最後の引数はマージ関数です。 v1エントリからidフィールドを選択し、v2から名前を選択することにより、重複キーの問題を解決します。
プログラムの実行後に出力されたmap3インスタンス:
George=Employee{id=2, name='George'}
John=Employee{id=8, name='John'}
Annie=Employee{id=22, name='Annie'}
Henry=Employee{id=1, name='Henry'}
6. シンプルなストリーミング
さらに、stream() pipelineを使用してマップエントリをアセンブルできます。 以下のコードスニペットは、重複するエントリを無視して、map2とmap1からエントリを追加する方法を示しています。
Map map3 = map2.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> new Employee(v1.getId(), v2.getName()),
() -> new HashMap<>(map1)));
予想どおり、マージ後の結果は次のとおりです。
{John=Employee{id=8, name='John'},
Annie=Employee{id=22, name='Annie'},
George=Employee{id=2, name='George'},
Henry=Employee{id=1, name='Henry'}}
7. StreamEx
JDKが提供するソリューションに加えて、人気のあるStreamEx libraryを使用することもできます。
簡単に言えば、StreamEx is an enhancement for the Stream APIであり、多くの追加の便利なメソッドを提供します。 EntryStream instance to operate on key-value pairsを使用します:
Map map3 = EntryStream.of(map1)
.append(EntryStream.of(map2))
.toMap((e1, e2) -> e1);
アイデアは、マップのストリームを1つにマージすることです。 次に、エントリを新しいmap3インスタンスに収集します。 重複するキーを処理するためのルールを定義するのに役立つため、(e1, e2) → e1式は言及することが重要です。 これがないと、コードはIllegalStateExceptionをスローします。
そして今、結果:
{George=Employee{id=2, name='George'},
John=Employee{id=8, name='John'},
Annie=Employee{id=22, name='Annie'},
Henry=Employee{id=1, name='Henry'}}
8. 概要
この短い記事では、Java 8でマップをマージするさまざまな方法を学びました。 より具体的には、we used Map.merge(), Stream API, StreamEx library。
いつものように、ディスカッション中に使用されたコードはover on GitHubにあります。