【Java学習|実務向け】Stream APIの強力な味方!Collectors.mapping()とflatMapping()を使いこなす

はじめに:Stream APIの表現力を格段に高める下流コレクター

Java 8で導入されたStream APIは、コレクション処理を宣言的かつ効率的に記述できる強力なツールです。しかし、単に要素をフィルタリングしたり変換したりするだけでなく、より複雑な集計や変換を行いたい場合、Stream APIの標準的なコレクターだけでは表現しきれないことがあります。

ここで登場するのが、`Collectors.mapping()`と`Collectors.flatMapping()`といった「下流コレクター」です。これらのメソッドは、既存のコレクターを「ラップ」することで、ストリームの各要素に対して追加の変換を適用してから、最終的な集計を行います。これにより、コードの可読性と表現力を飛躍的に向上させることができます。

特に、以下のような課題に直面した際に、これらの下流コレクターは非常に役立ちます。

  • 要素の変換と集計を同時に行いたい: 例えば、オブジェクトの特定のフィールドだけを取り出してリスト化したい場合。
  • ネストされたコレクションをフラット化したい: 例えば、リストのリストを一つのリストにまとめたい場合。

本記事では、これらの下流コレクターの基本的な使い方から、実用的なサンプルコード、そして現場で役立つ注意点までを、シニアJavaエンジニアの視点から分かりやすく解説していきます。

基礎知識:下流コレクターとは何か?

下流コレクター(downstream collector)という言葉は、主に`Collectors.groupingBy()`や`Collectors.partitioningBy()`といった「一级コレクター」と組み合わせて使用される場合に登場します。

  • 一级コレクター: ストリームの要素をグループ化したり、分割したりするコレクターです。例えば`Collectors.groupingBy(Function.identity())`は、要素をそのままキーとしてグループ化します。
  • 下流コレクター: 一级コレクターによってグループ化された各グループに対して、さらに適用されるコレクターです。これにより、グループごとの集計や変換が可能になります。

`Collectors.mapping()`と`Collectors.flatMapping()`は、この下流コレクターの概念を、より汎用的に単一のストリームに対しても適用できるようにしたものです。

Collectors.mapping()

`Collectors.mapping(Function mapper, Collector downstream)`

このメソッドは、ストリームの各要素`T`に対して、まず`mapper`関数を適用して`U`型の要素に変換します。その後、変換された`U`型の要素を、指定された`downstream`コレクターで収集します。

要するに、「各要素を変換してから、その変換された要素を(別の)コレクターで集める」という操作を行います。

Collectors.flatMapping()

`Collectors.flatMapping(Function> mapper, Collector downstream)`

`flatMapping`は、`mapping`と似ていますが、`mapper`関数が`Stream`を返す点が異なります。`flatMapping`は、`mapper`関数が返したストリームの要素をすべて平坦化(フラット化)し、それらをまとめて`downstream`コレクターで収集します。

これは、各要素が複数の要素に展開される可能性がある場合に非常に便利です。例えば、あるオブジェクトが複数のタグを持っている場合に、それらのタグをすべて抽出し、一つのリストにまとめたい場合などに使用します。

実装/解決策:具体的な使い方とサンプルコード

それでは、具体的なコード例を見ていきましょう。

Collectors.mapping() の活用例

例えば、`Person`オブジェクトのリストがあり、各`Person`の`name`だけを抽出して`Set`に格納したい場合を考えます。

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

@Override
public String toString() {
return “Person{” +
“name='” + name + ‘\” +
“, age=” + age +
‘}’;
}
}

public class MappingExample {
public static void main(String[] args) {
List people = List.of(
new Person(“Alice”, 30),
new Person(“Bob”, 25),
new Person(“Charlie”, 35),
new Person(“Alice”, 28) // 同じ名前の人がいてもOK
);

// mapping() を使って、Personオブジェクトから名前だけを抽出し、Setに集める
Set names = people.stream()
.collect(Collectors.mapping(
Person::getName, // 各Personオブジェクトから名前(String)を抽出するマッパー関数
Collectors.toSet() // 抽出した名前をSetに収集する下流コレクター
));

System.out.println(“抽出された名前のセット: ” + names);
// 出力例: 抽出された名前のセット: [Alice, Bob, Charlie]
}
}

この例では、`Person::getName`という関数参照が`mapper`として渡され、各`Person`オブジェクトは`String`型の名前に変換されます。その後、`Collectors.toSet()`という下流コレクターによって、これらの名前が重複なく`Set`に集められます。

`mapping()`を使わない場合、以下のようなコードになります。

Set namesWithoutMapping = people.stream()
.map(Person::getName) // まずmapで名前を抽出
.collect(Collectors.toSet()); // その後、Setに集める

`mapping()`を使うことで、`collect()`メソッドの引数だけで「変換してから集める」という一連の操作を完結させることができ、コードがより宣言的になります。

Collectors.flatMapping() の活用例

次に、`flatMapping()`の例を見てみましょう。各`Course`オブジェクトが複数の`Student`オブジェクトのリストを持っているとします。これらの`Student`オブジェクトをすべて抽出し、一つのリストにまとめたい場合を考えます。

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

class Student {
private String name;

public Student(String name) {
this.name = name;
}

public String getName() {
return name;
}

@Override
public String toString() {
return “Student{” +
“name='” + name + ‘\” +
‘}’;
}
}

class Course {
private String courseName;
private List students;

public Course(String courseName, List students) {
this.courseName = courseName;
this.students = students;
}

public List getStudents() {
return students;
}

public String getCourseName() {
return courseName;
}
}

public class FlatMappingExample {
public static void main(String[] args) {
List courses = List.of(
new Course(“Math”, List.of(new Student(“Alice”), new Student(“Bob”))),
new Course(“Science”, List.of(new Student(“Charlie”))),
new Course(“History”, List.of(new Student(“David”), new Student(“Alice”)))
);

// flatMapping() を使って、すべてのコースから学生を抽出し、一つのリストに集める
List allStudents = courses.stream()
.collect(Collectors.flatMapping(
course -> course.getStudents().stream(), // 各CourseからStudentのStreamを返すマッパー関数
Collectors.toList() // flatMapされたStudentのStreamを一つのListに収集する下流コレクター
));

System.out.println(“すべての学生リスト: ” + allStudents);
// 出力例: すべての学生リスト: [Student{name=’Alice’}, Student{name=’Bob’}, Student{name=’Charlie’}, Student{name=’David’}, Student{name=’Alice’}]
}
}

この例では、`course -> course.getStudents().stream()`というラムダ式が`mapper`として渡され、各`Course`オブジェクトから`Stream`が生成されます。`flatMapping`は、これらの`Stream`をすべて平坦化し、一つの大きな`Stream`にします。そして、`Collectors.toList()`によって、そのストリームが`List`に収集されます。

`flatMapping()`を使わない場合、`flatMap`メソッドを直接使うことになります。

List allStudentsWithoutFlatMapping = courses.stream()
.flatMap(course -> course.getStudents().stream()) // flatMapでStreamを平坦化
.collect(Collectors.toList()); // Listに集める

`flatMapping()`は、`Collectors.groupingBy()`などと組み合わせて使うことで、より強力な表現力を発揮します。例えば、コースごとに学生の名前をリスト化するといった場合に、下流コレクターとして`flatMapping`を指定することができます。

応用・注意点:現場で役立つ補足情報

`mapping()`と`flatMapping()`の使い分け

  • `mapping()`: 要素を1対1で変換し、その結果を収集したい場合に使用します。変換後の要素の数が元の要素の数と一致する場合(あるいは、変換後の要素を重複なく収集したい場合など)に最適です。
  • `flatMapping()`: 要素を0個以上の要素に変換(展開)し、それらをまとめて収集したい場合に使用します。要素が複数の結果を生む可能性がある場合に強力です。

`Collectors.groupingBy()`との組み合わせ

これらの下流コレクターは、`Collectors.groupingBy()`と組み合わせて使うことで、より複雑な集計が可能になります。

例:コース名ごとに、そのコースに所属する学生の名前のセットを取得する

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

// 上記のPerson, Student, Courseクラスは省略

public class GroupingWithMappingExample {
public static void main(String[] args) {
List courses = List.of(
new Course(“Math”, List.of(new Student(“Alice”), new Student(“Bob”))),
new Course(“Science”, List.of(new Student(“Charlie”))),
new Course(“History”, List.of(new Student(“David”), new Student(“Alice”))),
new Course(“Math”, List.of(new Student(“Eve”))) // Mathコースにさらに学生を追加
);

// コース名でグループ化し、各コースの学生の名前のセットを取得する
Map> studentsPerCourse = courses.stream()
.collect(Collectors.groupingBy(
Course::getCourseName, // キー:コース名
Collectors.flatMapping( // 値:各グループ(コース)に対する下流コレクター
course -> course.getStudents().stream().map(Student::getName), // 各コースの学生から名前のStreamを生成
Collectors.toSet() // 生成された名前のStreamをSetに収集
)
));

System.out.println(“コースごとの学生名セット: ” + studentsPerCourse);
// 出力例: コースごとの学生名セット: {Math=[Alice, Eve, Bob], Science=[Charlie], History=[David, Alice]}
}
}

この例では、`groupingBy`の第二引数に`flatMapping`を指定しています。`flatMapping`は、各`Course`オブジェクトから、そのコースの学生の名前のStreamを生成し、それらを平坦化して`Collectors.toSet()`でSetに収集しています。

パフォーマンスに関する注意点

  • `mapping()`や`flatMapping()`自体がパフォーマンスのボトルネックになることは稀ですが、内部で呼び出されるマッパー関数や下流コレクターの処理が重い場合は、全体のパフォーマンスに影響します。
  • 特に`flatMapping()`で生成されるストリームが大きい場合や、ネストが深い場合は、メモリ使用量に注意が必要です。

Sequenced Collectionsとの連携

Java 21で導入されたSequenced Collections(`SequencedCollection`インターフェース、`SequencedSet`、`SequencedMap`など)は、要素の挿入順序や削除順序を保持するコレクションです。

`Collectors.mapping()`や`Collectors.flatMapping()`は、これらのSequenced Collectionsとも問題なく連携できます。例えば、`Collectors.toCollection(SequencedSet::new)`のように、Sequenced Collectionsを生成するコレクターを指定することができます。

import java.util.List;
import java.util.SequencedSet;
import java.util.stream.Collectors;

// Personクラスは上記と同じ

public class SequencedCollectionExample {
public static void main(String[] args) {
List people = List.of(
new Person(“Alice”, 30),
new Person(“Bob”, 25),
new Person(“Alice”, 28)
);

// mapping() と SequencedSet を使って、名前を順序を保ったSetに集める
SequencedSet orderedNames = people.stream()
.collect(Collectors.mapping(
Person::getName,
Collectors.toCollection(SequencedSet::new) // SequencedSetに収集
));

System.out.println(“順序を保った名前のセット: ” + orderedNames);
// 出力例: 順序を保った名前のセット: [Alice, Bob] (挿入順)
}
}

Sequenced Collectionsを使用することで、収集結果の順序を厳密に制御したい場合に役立ちます。

まとめ

`Collectors.mapping()`と`Collectors.flatMapping()`は、Stream APIの表現力を豊かにする非常に強力なツールです。

  • `mapping()`は、要素の変換と収集を簡潔に行いたい場合に。
  • `flatMapping()`は、要素を複数の要素に展開し、それらをまとめて収集したい場合に。

これらを`Collectors.groupingBy()`などの一级コレクターと組み合わせることで、より複雑で洗練されたデータ処理が可能になります。ぜひ、日々のコーディングでこれらの下流コレクターを活用し、より簡潔で可読性の高いJavaコードを記述してください。

コメント

タイトルとURLをコピーしました