1. 序章

このクイックチュートリアルでは、Java8機能を使用して2つのマップをマージする方法を示します。

具体的には、エントリが重複しているマップなど、さまざまなマージシナリオを検討します。

2. 初期化

まず、2つのMapインスタンスを定義しましょう。

private static Map<String, Employee> map1 = new HashMap<>();
private static Map<String, Employee> 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は、新しいmerge()関数をjava.util.Mapインターフェースに追加します。

merge()関数のしくみは次のとおりです。指定されたキーがまだ値に関連付けられていないか、値がnullの場合、キーは指定された値に関連付けられます。

それ以外の場合は、値を指定された再マッピング関数の結果に置き換えます。 再マッピング関数の結果がnullの場合、結果は削除されます。

まず、 map1 からすべてのエントリをコピーして、新しいHashMapを作成しましょう。

Map<String, Employee> 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'}

結果として、 結合されたマップには、以前のHashMapエントリのすべての要素が含まれています。 キーが重複しているエントリが1つのエントリにマージされました 。

また、最後のエントリのEmployeeオブジェクトにはmap1idがあり、値はmap2から選択されていることがわかります。

これは、マージ関数で定義したルールが原因です。

(v1, v2) -> new Employee(v1.getId(), v2.getName())

4. Stream.concat()

Java8のStream APIも、問題の簡単な解決策を提供できます。 まず、Mapインスタンスを1つのStreamに結合する必要があります。 これはまさにStream.concat()操作が行うことです。

Stream combined = Stream.concat(map1.entrySet().stream(), map2.entrySet().stream());

ここでは、マップエントリセットをパラメータとして渡します。 次に、結果を新しいMapに収集する必要があります。 そのために、 Collectors.toMap()を使用できます。

Map<String, Employee> result = combined.collect(
  Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

その結果、コレクターはマップの既存のキーと値を使用します。 しかし、このソリューションは完璧にはほど遠いです。 コレクターが重複キーを持つエントリに遭遇するとすぐに、IllegalStateExceptionをスローします。

この問題を処理するには、3番目の「マージ」ラムダパラメーターをコレクターに追加するだけです。

(value1, value2) -> new Employee(value2.getId(), value1.getName())

重複キーが検出されるたびにラムダ式を使用します。

最後に、すべてをまとめます。

Map<String, Employee> 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” を持つ重複エントリは、新しい従業員のIDがmap2から選択され、値がmap1から選択された新しいキーと値のペアにマージされました。 ]。

5. Stream.of()

Stream APIを引き続き使用するには、 Stream.of()を使用して、Mapインスタンスを統合ストリームに変換できます。

ここでは、ストリームを操作するために追加のコレクションを作成する必要はありません。

Map<String, Employee> 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())));

まず、map1とmap2を単一のストリームに変換します。 次に、ストリームをマップに変換します。 ご覧のとおり、 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()パイプラインを使用してマップエントリをアセンブルできます。 以下のコードスニペットは、重複するエントリを無視して、map2およびmap1からエントリを追加する方法を示しています。

Map<String, Employee> 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ライブラリも使用できます。

簡単に言えば、StreamExはStreamAPI の拡張機能であり、多くの追加の便利なメソッドを提供します。 EntryStreamインスタンスを使用して、キーと値のペアを操作します

Map<String, Employee> 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. 概要

この短い記事では、Java8でマップをマージするさまざまな方法を学びました。 具体的には、 Map.merge()、Stream API、StreamExライブラリを使用しました。

いつものように、ディスカッション中に使用されたコードは、GitHubにあります。