- Map へ変換(キー重複無し)
- Map へ変換(キー重複有り)
- 値が null 値となる場合に注意
- LinkedHashMap へ変換
- 任意キーでグルーピング
- 任意キーでグルーピングしてマッピング
- グルーピングして集計
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 が奇数か偶数かにより分類する例です。