0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Effective Java 7.42:ラムダとストリーム

Posted at

7.42:ラムダとストリーム

要約

ラムダとストリームはコードを簡潔で宣言的にし、並列化の扉も開く強力な道具。だが可読性・副作用・性能に注意して、適材適所で使うべき。

見本

短くて意図が分かりやすい — これがラムダ&ストリームの強み。

// 従来(命令型)
List<String> names = new ArrayList<>();
for (Person p : people) {
    if (p.getAge() >= 20) names.add(p.getName().toUpperCase());
}

// ストリーム+ラムダ(宣言型)
List<String> names = people.stream()
    .filter(p -> p.getAge() >= 20)
    .map(p -> p.getName().toUpperCase())
    .collect(Collectors.toList());

良いパターン(推奨)

  • 標準の関数型インタフェースを使う: Function, Predicate, Consumer,Supplier
Function<Employee, String> getName = Employee::name;          // Function<T,R>
Predicate<Employee> isAdult = e -> e.age() >= 20;             // Predicate<T>
Consumer<Employee> printer = e -> System.out.println(e.name()); // Consumer<T>
Supplier<List<Employee>> supplier = ArrayList::new;           // Supplier<T>

List<Employee> list = List.of(
    new Employee("Alice", 30, Department.ENG),
    new Employee("Bob",   19, Department.SALES),
    new Employee("Carol", 25, Department.ENG)
);
list.stream()
    .filter(isAdult)
    .map(getName)
    .forEach(printer);
  • メソッド参照を活用: map(Person::getName) は読みやすい
// map(e -> e.name()) の代わりに
List<String> names = list.stream()
    .map(Employee::getName)   // メソッド参照
    .collect(Collectors.toList());
  • プリミティブ専用ストリームを使うIntStream/LongStream)でボクシングを避ける
// 年齢の合計(ボクシングを避ける)
int totalAge = list.stream()
    .mapToInt(Employee::age)   // IntStream
    .sum();

// 平均
OptionalDouble avg = list.stream()
    .mapToInt(Employee::age)
    .average();
  • Collectors を使った集約: Collectors.toList(), groupingBy, partitioningBy, toMap
// toList
List<Employee> engs = list.stream()
    .filter(e -> e.department() == Department.ENG)
    .collect(Collectors.toList());

// toMap (キー重複時の解決)
Map<String, Employee> byName = list.stream()
    .collect(Collectors.toMap(Employee::name, Function.identity(),
        (existing, replacement) -> existing)); // 衝突は先のものを採る

// groupingBy(次のセクションで詳述)
Map<Department, List<Employee>> byDept = list.stream()
    .collect(Collectors.groupingBy(Employee::department));

// partitioningBy(Predicate による2分割)
Map<Boolean, List<Employee>> adultsPartition = list.stream()
    .collect(Collectors.partitioningBy(e -> e.age() >= 20));
  • 短絡(short-circuit)オペレーションを利用: anyMatch, allMatch, findFirst, limit
boolean anyAdult = list.stream().anyMatch(e -> e.age() >= 20); // 早期終了可
boolean allAdults = list.stream().allMatch(e -> e.age() >= 20);
Optional<Employee> firstEng = list.stream().filter(e -> e.department()==Department.ENG).findFirst();
List<Employee> limited = list.stream().limit(2).collect(Collectors.toList());
  • 副作用を避ける: map/filter 内で状態を変更しない。最終段の forEach で副作用するなら限定的に
Map<Department, List<Employee>> byDepartment =
    list.stream()
        .collect(Collectors.groupingBy(Employee::department));

// さらに各部門の年齢平均を求める例
Map<Department, Double> avgAgeByDept =
    list.stream()
        .collect(Collectors.groupingBy(Employee::department,
                 Collectors.averagingInt(Employee::age)));

// 出力例
byDepartment.forEach((dept, emps) ->
    System.out.println(dept + ": " + emps.stream().map(Employee::name).collect(Collectors.joining(", ")))
);

avgAgeByDept.forEach((dept, avg) ->
    System.out.println(dept + " average age: " + avg)
);

悪いパターン

  • ストリームで副作用
    スレッド安全でない可変変数への書き込みなど
List<Integer> result = new ArrayList<>();
items.stream().forEach(i -> result.add(i)); // NG:副作用、スレッド安全性がない
  • 並列ストリームで可変状態を共有parallelStream()ArrayList を同時に操作するなど)
Map<String, Long> counts = new HashMap<>();
items.parallelStream().forEach(i -> counts.merge(i.type(), 1L, Long::sum)); // race!
  • ストリームを2回使おうとする
    ストリームは単回使用
Stream<String> s = items.stream().filter(...);
s.forEach(...);
s.forEach(...); // IllegalStateException
  • チェック例外をラムダ内で直接投げる
    コンパイルの扱いが面倒
items.stream().map(s -> {
    return Files.readString(Paths.get(s)); // throws IOException -> compile error
});
  • 無意味に複雑なパイプライン
    1 行が長すぎて読みづらい
List<String> r = items.stream()
    .filter(i -> i.isActive() && i.getDate().isAfter(someDate) && complexCheck(i))
    .map(i -> i.getSub().flatMap(x -> x.map(y -> compute(y)).orElse("X")))
    .flatMap(s -> Arrays.stream(s.split(",")))
    .distinct().sorted()
    .collect(Collectors.toList());

まとめ

  • 副作用は避け、結果は collect で受け取る
  • 並列ストリームで共有ミュータブル状態を書き換えない
  • ストリームは一度しか使えない ➡ 必要なら Supplier<Stream>collect で保存
  • ラムダでチェック例外が出る処理はラップするか、従来ループで書く
  • 長いチェーンはメソッド抽出で読みやすく
  • parallel() は測定してから使う
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?