Java 8 でグルーピング処理 - List<V> を Map<K, V> へ変換

Java 8 で List<V>Map<K, V> へ変換するようなグルーピング処理をいくつか試してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150420/

はじめに

今回は、下記をリスト化した List<Data>id でグルーピングして Map<String, Data> へ変換します。

class Data {
    private String id;
    private String name;

    public Data(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }
    ・・・
}

Java 8 より前のバージョンでは以下のようにすると思います。

拡張 for 利用
List<Data> dataList = Arrays.asList(
    new Data("d1", "sample1"),
    new Data("d2", "sample2"),
    new Data("d3", "sample3")
);

Map<String, Data> res = new HashMap<>();
for (Data d : dataList) {
    res.put(d.getId(), d);
}

また、Java 8 で Map<String, List<Data>> へ変換するなら Collectors.groupingBy を使うだけです。

groupingBy で Map<String, List > へ変換
Map<String, List<Data>> res = dataList.stream().collect(
    Collectors.groupingBy(Data::getId)
);

(1) forEach

まずは、拡張 for の変わりに forEach メソッドを使用する方法です。

Map<String, Data> res = new HashMap<>();
dataList.forEach(d -> res.put(d.getId(), d));

(2) toMap

次は、Collectors.toMap を使用する方法です。

toMap の 2引数版

Map<String, Data> res = dataList.stream().collect(
    Collectors.toMap(Data::getId, d -> d)
);

もしくは

Map<String, Data> res = dataList.stream().collect(
    Collectors.toMap(Data::getId, UnaryOperator.identity())
);

ここで、2引数版の toMap メソッドには以下のような注意点があります。

  • 同一キーを持つオブジェクトを複数含んでいると IllegalStateException を throw する

例えば、以下は IllegalStateException となります。

IllegalStateException が発生するコード例
List<Data> dataList2 = Arrays.asList(
    new Data("d1", "sample1"),
    new Data("d2", "sample2"),
    new Data("d3", "sample3"),
    new Data("d1", "sample1-b") // d1 が重複
);

// IllegalStateException: Duplicate key Data(d1, sample1) が発生
Map<String, Data> res = dataList2.stream().collect(
    Collectors.toMap(Data::getId, d -> d)
);
IllegalStateException エラー内容
Exception in thread "main" java.lang.IllegalStateException: Duplicate key Data(d1, sample1)

IllegalStateException を発生させないようにするには、3引数版の toMap を使います。

toMap の 3引数版

第 3引数で同一キーの値が複数あった場合にどちらを選択するかを指定します。

最初の要素を採用する場合
// 結果 [ d1: Data(d1, sample1), d2: Data(d2, sample2), d3: Data(d3, sample3) ]
Map<String, Data> res = dataList2.stream().collect(
    Collectors.toMap(Data::getId, d -> d, (d1, d2) -> d1)
);
最後の要素を採用する場合
// 結果 [ d1: Data(d1, sample1-b), d2: Data(d2, sample2), d3: Data(d3, sample3) ]
Map<String, Data> res = dataList2.stream().collect(
    Collectors.toMap(Data::getId, d -> d, (d1, d2) -> d2)
);

(3) groupingBy + collectingAndThen

あまり実用的では無いと思いますが、groupingBy と collectingAndThen を組み合わせる方法も考えられます。

// 結果 [ d1: Data(d1, sample1), d2: Data(d2, sample2), d3: Data(d3, sample3) ]
Map<String, Data> res = dataList2.stream().collect(
    Collectors.groupingBy(
        Data::getId,
        Collectors.collectingAndThen(
            Collectors.toList(),
            a -> a.get(0) //最初の要素を採用
        )
    )
);

toList を minBy 等で代用する事も可能です。

Map<String, Data> res = dataList2.stream().collect(
    Collectors.groupingBy(
        Data::getId,
        Collectors.collectingAndThen(
            Collectors.minBy((a, b) -> 0),
            a -> a.get()
        )
    )
);

(4) collect の 3引数版

最後に、3引数版の collect を使う方法です。

パラレル実行で使用する第 3引数が必須となっている点が微妙だと思います。 (下記 Map::putAll の箇所を null にすると NullPointerException となります)

Map<String, Data> res = dataList.stream().collect(
    HashMap::new,
    (m, d) -> m.put(d.getId(), d),
    Map::putAll
);