現場で良く使う Java Stream イディオム

開発の現場でよく使う Java Stream イディオムです。


キャスト

List<String> studentNames = people.stream()
    .filter(Student.class::isInstance)
    .map(Student.class::cast)
    .map(Student::getName)
    .collect(Collectors.toList());


null 除外

List<String> names = people.stream()
    .map(People::getName)
    .filter(Objects::nonNull)
    .collect(Collectors.toList());


否定フィルタ

Java11~

int count = strings.stream()
    .filter(Predicate.not(String::isEmpty)).count();

それ以前

int count = strings.stream()
    .filter(s -> !s.isEmpty())).count();


Streamから配列へ

stream.toArray(T[]::new);

プリミティブな Stream(IntStream など)の場合は以下

IntStream.rangeClosed(0, 3).toArray(); // [0, 1, 2, 3]


配列からStreamへ

String[] array = //...
Stream.of(array)

プリミティブ配列の場合は以下

int[] array = //...
Arrays.stream(array)


リストの集約

List<List<String>> list = new ArrayList<>();
list.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList()); // List<String>

配列の場合は以下

List<String[]> list = List.of("ab,cd,ef".split(","), "12,34,56".split(","));
List<String> result = list.stream()
    .flatMap(Stream::of)
    .collect(Collectors.toList()); // [ab,cd,ef,12,34,56]


リストからマップへ変換

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

キー重複がある場合は IllegalStateException となるため以下のようにする必要がある(以下は後勝ちの例)

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

値に null が有る場合、Collectors.toMap()NullPointerException となるため以下のようにする必要がある

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

並びを維持した LinkedHashMap への変換

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


コレクションの変更

Set<String> set = list.stream()
    .collect(Collectors.toCollection(LinkedHashSet::new));


任意キーでグルーピング

Map<String, List<Item>> m = items.stream()
    .collect(Collectors.groupingBy(Item::getCode));


任意キーでソート

list.stream()
    .sorted(Comparator.comparing(Item::getId))
    .collect(Collectors.toList());

複合キーのソート

list.stream()
    .sorted(Comparator.comparing(Item::getId)
                      .thenComparing(Item::getName))
    .collect(Collectors.toList());

ソートキーに null を含む場合

list.stream()
    .sorted(Comparator.comparing(Item::getName,
                         Comparator.nullsFirst(Comparator.naturalOrder())))
    .collect(Collectors.toList());

任意順序でのソート

List<String> priorities = Arrays.asList("top", "middle", "bottom");
List<Item> sorted = list.stream()
    .sorted(Comparator.comparing(
            (Item item) -> priorities.indexOf(item.getName())))
    .collect(Collectors.toList());


カンマ区切り

numbers.stream()
       .map(Object::toString)
       .collect(Collectors.joining(", "));


プリミティブRangeからリストへ

IntStream.rangeClosed(10, 20).boxed().collect(Collectors.toList());


オブジェクト型の合計

int sum = integers.stream()
    .mapToInt(Integer::intValue)
    .sum();
Integer sum = integers.stream()
    .reduce(0, Integer::sum);


BigDecimal の合計

BigDecimal sum = numbers.stream()
    .reduce(BigDecimal.ZERO, BigDecimal::add);


グルーピングして集計

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

なお、reducing の第一引数である初期値は、stream処理全体で1回取得したものが流用される。そのため BigDecimal::add のように、結果として新しいオブジェクトを生成するメソッドを使わなければならない(ミュータブルな操作を行った場合、異なるグルーピング間で初期インスタンスが共有されてしまいおかしなことになる)。


グルーピングしてカウント

Map<LocalDate, Long> count = list.stream().collect(
  Collectors.groupingBy(
    Item::getSalesDate,
    Collectors.counting()));


最大値/最小値の抽出

List<LocalDate> dates = Arrays.asList(
        LocalDate.of(2023, 1, 2),
        LocalDate.of(2023, 1, 1),
        null,
        LocalDate.of(2023, 1, 4),
        LocalDate.of(2023, 1, 3));

LocalDate min = dates.stream().min(Comparator.nullsLast(Comparator.naturalOrder()))
        .orElseThrow(NoSuchElementException::new);
// 2023-01-01

Comparator.nullsFirst(Comparator.naturalOrder()) とした場合は NullPointerException となる。

オブジェクトのフィールドでソートする場合には以下のようになる。

.min(Comparator.comparing(Person::getAge))


Optional から値の取り出し

Optional::isPresent である中身を抽出。

List<Optional<String>> optionals = Arrays.asList(
    Optional.of("foo"),
    Optional.empty(),
    Optional.of("bar"));

Java9~

List<String> names = optionals.stream()
  .flatMap(Optional::stream)
  .collect(Collectors.toList());

それ以前

List<String> names = optionals.stream()
  .flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty))
  .collect(Collectors.toList());