Java Tips | コレクション:フラット化

Java Java
スポンサーリンク

フラット化は「入れ子の一覧を、一本の一覧に伸ばす」技

フラット化は、ざっくり言うと
「List<List<T>> のような“入れ子のコレクション”を、List<T> という“まっすぐなコレクション”に伸ばす」処理です。

部署ごとの社員一覧(List<List<Employee>>)を「全社員一覧」にしたい。
注文ごとの明細一覧(List<List<OrderLine>>)を「全明細一覧」にしたい。
ユーザーごとの権限一覧(List<List<Role>>)を「全権限一覧」にしたい。

こういう「二重・三重の入れ子 → 一本の一覧」の変換を、
安全に・読みやすく・再利用しやすく書くための考え方が“フラット化”です。


一番基本:List<List<Integer>> を一本の List<Integer> にする

flatMap のイメージをつかむ

まずは、数値の入れ子リストをフラット化する一番シンプルな例から。

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

public class FlattenBasic {

    public static void main(String[] args) {
        List<List<Integer>> nested = List.of(
                List.of(1, 2),
                List.of(3),
                List.of(4, 5)
        );

        List<Integer> flat =
                nested.stream()
                      .flatMap(list -> list.stream())
                      .collect(Collectors.toList());

        System.out.println(flat); // [1, 2, 3, 4, 5]
    }
}
Java

ここでの一番大事なポイントは flatMap です。

map は「1 要素 → 1 要素」に変換しますが、
flatMap は「1 要素 → 0 個以上の要素」に変換し、それらを全部つなげて一本のストリームにしてくれます。

List<List<Integer>> の 1 要素は List<Integer> です。
それを list -> list.stream() で「ストリーム」に変換し、
flatMap がそれらを全部つなげて「Integer のストリーム」にしてくれる、というイメージです。


フラット化ユーティリティを作る

「毎回 flatMap と collect を書きたくない」を解消する

同じようなフラット化を何度も書くなら、
ユーティリティメソッドにしてしまうとスッキリします。

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

public final class Flattens {

    private Flattens() {}

    public static <T> List<T> flatten(
            Collection<? extends Collection<T>> source
    ) {
        if (source == null || source.isEmpty()) {
            return List.of();
        }
        return source.stream()
                     .filter(c -> c != null && !c.isEmpty())
                     .flatMap(c -> c.stream())
                     .collect(Collectors.toList());
    }
}
Java

使い方はこうです。

List<List<Integer>> nested = List.of(
        List.of(1, 2),
        List.of(3),
        List.of(4, 5)
);

List<Integer> flat = Flattens.flatten(nested);
Java

ここでの重要ポイントは二つです。

一つ目は、「外側も内側も null や空コレクションを安全にスキップしている」ことです。
filter(c -> c != null && !c.isEmpty()) によって、
「null の中身を stream() して NPE」という事故を防いでいます。

二つ目は、「Collection<? extends Collection<T>> としておくことで、List でも Set でも使える汎用ユーティリティになっている」ことです。
「入れ子のコレクションを一本にする」という意図だけを、きれいに切り出せています。


業務っぽい例:部署ごとの社員一覧 → 全社員一覧

ネストしたオブジェクト構造をフラット化する

もう少し業務寄りの例を見てみます。

import java.util.List;

class Employee {
    private final String name;
    public Employee(String name) {
        this.name = name;
    }
    public String getName() { return name; }
}

class Department {
    private final String name;
    private final List<Employee> members;
    public Department(String name, List<Employee> members) {
        this.name = name;
        this.members = members;
    }
    public String getName() { return name; }
    public List<Employee> getMembers() { return members; }
}
Java

部署ごとの社員一覧から、「全社員一覧」を作ります。

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

public class DepartmentFlattenSample {

    public static void main(String[] args) {
        Department dev = new Department(
                "開発",
                List.of(new Employee("山田"), new Employee("佐藤"))
        );
        Department sales = new Department(
                "営業",
                List.of(new Employee("鈴木"))
        );

        List<Department> departments = List.of(dev, sales);

        List<Employee> allEmployees =
                departments.stream()
                           .flatMap(d -> d.getMembers().stream())
                           .collect(Collectors.toList());

        System.out.println(allEmployees.size()); // 3
    }
}
Java

ここでの重要ポイントは、
Department 一つから List<Employee> を取り出し、それを flatMap でつなげている」ことです。

map(d -> d.getMembers()) だけだと List<List<Employee>> になりますが、
flatMap(d -> d.getMembers().stream()) にすることで、
「全社員を一本のストリームにしている」わけです。

このパターンは、
「親オブジェクト一覧 → 子オブジェクト一覧」に変換したいときに頻出します。


フラット化+マッピング:よくある実務パターン

例:部署ごとの社員一覧 → 全社員の名前一覧

フラット化は、マッピングと組み合わせるとさらに威力を発揮します。

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

List<String> allEmployeeNames =
        departments.stream()
                   .flatMap(d -> d.getMembers().stream()) // フラット化
                   .map(Employee::getName)                // マッピング
                   .collect(Collectors.toList());
Java

ここでの重要ポイントは、
「まずフラット化で“対象(Employee)”を一本にし、そのあとで“形(名前)”を変えている」ことです。

流れとしてはこうです。

部署一覧(Department)
→ 部署ごとの社員一覧(List<Employee>)
→ 全社員一覧(Employee)
→ 全社員の名前一覧(String)

この「フラット化 → マッピング」の流れを意識できると、
ネストしたデータ構造を扱うのが一気に楽になります。


null や空リストを含むときの考え方

「null 部署」「社員リストが null の部署」が混ざるケース

現実のデータでは、
「部署自体が null」「部署はあるが社員リストが null」というケースもありえます。

その場合、フラット化ユーティリティ側で安全に扱っておくと、
呼び出し側がかなり楽になります。

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

public final class Flattens {

    private Flattens() {}

    public static <T> List<T> flatten(
            Collection<? extends Collection<T>> source
    ) {
        if (source == null || source.isEmpty()) {
            return List.of();
        }
        return source.stream()
                     .filter(Objects::nonNull)
                     .flatMap(c -> c == null ? List.<T>of().stream() : c.stream())
                     .collect(Collectors.toList());
    }
}
Java

あるいは、Department 専用にこう書いてもよいです。

public static List<Employee> flattenMembers(List<Department> departments) {
    if (departments == null || departments.isEmpty()) {
        return List.of();
    }
    return departments.stream()
                      .filter(Objects::nonNull)
                      .flatMap(d -> {
                          List<Employee> members = d.getMembers();
                          return members == null ? List.<Employee>of().stream()
                                                 : members.stream();
                      })
                      .collect(Collectors.toList());
}
Java

ここでの重要ポイントは、
「null の扱いも“フラット化ルールの一部”」だと意識することです。

「null 部署は無視する」「社員リストが null の部署は社員 0 人とみなす」など、
プロジェクトとしての方針をユーティリティに閉じ込めておくと、
呼び出し側は「とにかく全社員一覧が欲しい」とだけ考えればよくなります。


まとめ:フラット化ユーティリティで身につけてほしい感覚

フラット化は、
単に「List<List<T>> を List<T> にするテクニック」ではなく、
「ネストしたデータ構造を、扱いやすい“一本の流れ”に変える技術」です。

flatMap(コレクション → stream()) の基本形に慣れる。
Flattens.flatten(入れ子コレクション) のようなユーティリティにして、null や空を一箇所で処理する。
親オブジェクト一覧から子オブジェクト一覧を取り出すときに、「map ではなく flatMap だな」と判断できるようになる。
フラット化 → マッピング(→ フィルタ → 集計)という流れで、「何を対象にして、どう加工するか」を意識する。

あなたのコードのどこかに、
二重の for 文で「部署を回して、その中で社員を回して、新しい List に add している」箇所があれば、
それを一度「フラット化ユーティリティ+flatMap」に置き換えられないか眺めてみてください。

その小さな一歩が、
「ネストしたデータを、迷いなく“まっすぐ”にできるエンジニア」への、
確かなステップになります。

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