Java Stream でよく使う Map 変換

f:id:Naotsugu:20191225000550p:plain


Map へ変換(キー重複無し)

以下の Item のリストを id をキーにした Map に変換します。

List<Item> list = Arrays.asList(
    new Item(9, "apple"), 
    new Item(3, "lemon"), 
    new Item(6, "peach"));


collect()Collectors.toMap を指定した終端処理で Map に変換することができます。

list.stream().collect(
    Collectors.toMap(Item::getId, e -> e));

// {3 = Item{3, 'lemon'}, 6 = Item{6, 'peach'}, 9 = Item{9, 'apple'}}


上記は UnaryOperator.identity() を使って以下のように書いても同じです。

list.stream().collect(
    Collectors.toMap(Item::getId, UnaryOperator.identity()));


UnaryOperator は単項演算用の関数インターフェースで、identity() は引数をそのまま返す定義となっています。

public interface UnaryOperator<T> extends Function<T, T> {
    static <T> UnaryOperator<T> identity() { return t -> t; }
}


Item の中身を展開する場合は以下のようにすることができます。

list.stream().collect(
    Collectors.toMap(Item::getId, Item::getName)));

// {3 = lemon, 6 = peach, 9 = apple}


Map へ変換(キー重複有り)

キーが重複する場合は注意が必要です。

以下の例では、id の 6 が重複します。

List<Item> list = Arrays.asList(
    new Item(9, "apple"), 
    new Item(3, "lemon"), 
    new Item(6, "peach"), 
    new Item(6, "banana")); // duplicate

toMap() の第三引数を指定しない場合は、キー重複時に java.lang.IllegalStateException: Duplicate key Item{6, 'peach'} の例外がスローされます。

list.stream().collect(
    Collectors.toMap(Item::getId, e -> e));

// java.lang.IllegalStateException: Duplicate key Item{6, 'peach'}


キー重複する場合は、重複時にどちらを採用するかを、toMap() の第三引数で指定する必要があります。

以下は先勝ちとする例です。

list.stream().collect(
    Collectors.toMap(Item::getId, e -> e, (e1, e2) -> e1));

// {3=Item{3, 'lemon'}, 6=Item{6, 'peach'}, 9=Item{9, 'apple'}}


値が null 値となる場合に注意

Collectors.toMap() による Map 変換は、値が null となる場合に NullPointerException となるため注意が必要です。

List<Item> list = Arrays.asList(
    new Item(9, "apple"), 
    new Item(3, null), 
    new Item(6, "peach"));
list.stream().collect(
    Collectors.toMap(Item::getId, Item::getName)));
// java.lang.NullPointerException

多くの予想に反して、キー が null の場合は NullPointerException とはならず、値が null の場合に NullPointerException となります。

この問題は Java8 時代から、JDK のバグトラッカーhttps://bugs.openjdk.java.net/browse/JDK-8148463 でオープン状態のままになっています。

値が null となる可能性がある場合は、Collectors.toMap() は使わず、以下のようにすれば回避できます。

list.stream().collect(
    HashMap::new, (Map m, Item i) -> m.put(i.getId(), i.getValue()), Map::putAll)

// {3 = null, 6 = peach, 9 = apple}


LinkedHashMap へ変換

並びを維持したい場合は、第四引数の mapSupplier に LinkedHashMap::new を指定します。

list.stream().collect(
    Collectors.toMap(Item::getId, e -> e, (e1, e2) -> e1, LinkedHashMap::new)));

// {9 = Item{9, 'apple'}, 3 = Item{3, 'lemon'}, 6 = Item{6, 'peach'}}


任意キーでグルーピング

特定の条件でグルーピングする場合は Collectors.groupingBy を使います。

Map<Integer, List<Item>> m = list.stream().collect(
    Collectors.groupingBy(Item::getId));

// {3 = [Item{3, 'lemon'}], 
//  6 = [Item{6, 'peach'}, Item{6, 'banana'}], 
//  9 = [Item{9, 'apple'}]}

id が同じものをグルーピングできました。


任意キーでグルーピングしてマッピング

グルーピングする値をマッピングした値にすることもできます。

Map<Integer, List<String>> m = list.stream().collect(
    Collectors.groupingBy(
            Item::getId,
            Collectors.mapping(Item::getName, Collectors.toList())));

Collectors.groupingBy の第2引数にマッピング関数を渡します。


グルーピングして集計

売上日別の価格集計などは Collectors.reducing で畳み込みを行います。

Map<LocalDate, BigDecimal> m = list.stream().collect(
    Collectors.groupingBy(
            Item::getSalesDate,
            Collectors.reducing(BigDecimal.ZERO, Item::getPrice, BigDecimal::add)));

groupingBy() の第二引数に Collectors.reducing を指定して集計しています。


グループ分け

Collectors.partitioningBy はある条件で2つのグループに分ける場合に利用します。

Map<Boolean, List<Item>> m = list.stream().collect(
    Collectors.partitioningBy(item -> item.getId() % 2 == 0));

// {false=[Item{id=9, name='apple'}, Item{id=3, name='lemon'}], 
//  true=[Item{id=6, name='peach'}, Item{id=6, name='banana'}]}

id が奇数か偶数かにより分類する例です。



Effective Java

Effective Java

Amazon