Java Stream API 使い方めも|ListとMap変換からよく使うパターンまで【Java 8〜17対応】

当ページのリンクには広告が含まれています。
Java Stream APIの基本構文からfilter・map・collect・groupingByまで、コレクション操作を効率化する実践的な使い方を解説

Stream APIって便利なんですが「さて、どう書くんだっけ?」ってなることありますよね。

特にListとMapを行き来する処理は、一度覚えてしまえばあとは応用するだけなのですが久しぶりに書くと手が止まりがちです。何なら毎回検索してる気がする。

このメモを3歩歩いても忘れる筆者のために残しておきます。同じく「あれ、どう書くんだっけ」となった方の助け舟にもなれば。

目次

この記事でわかること

  • Collectors.toMap()を使ってListからMapへ変換する方法
  • keySet()やStreamを使ってMapのキーをListへ変換する方法
  • filter / map / flatMapなどよく使うStream処理のパターン
  • Collectorsの早見表
  • Java 8〜17での書き方の違いと注意点

for文 vs Stream

比較Stream APIfor文
可読性◎ 高い(※1)〇 普通
大量データ処理◎ 得意〇 普通
デバッグ やや難しい〇 簡単
学習コスト やや高い〇 低い
並列処理〇 可能× 難しい

※1 とはいえ無理に1行にする、ネストしすぎると可読性、保守性が落ちます。

Stream APIの基本的な流れ

まず全体のイメージを掴んでおきましょう。StreamはCollectionや配列をパイプライン的に処理する仕組みで、3段階で動きます。

  1. ソースlist.stream()でStreamを生成する。
  2. 中間操作filter() / map() / sorted()など、処理を繋げる(実際にはまだ動かない)。
  3. 終端操作collect() / count() / findFirst()など、これを呼んで初めて処理が走る。

「中間操作は遅延評価される(実際には終端操作が来るまで動かない)」というのがStreamの特徴です。これを知っておくと、パフォーマンスのイメージが掴みやすくなりますよ。

ListからMapへ変換する

基本の書き方

Collectors.toMap()を使います。第1引数にキー、第2引数に値を指定するだけです。

Java
Map<K, V> map = list
    .stream()
    .collect(Collectors.toMap(keyMapper, valueMapper));

具体例

Userクラスのリストをidをキー・nameを値にしたMapへ変換するパターンです。

Java
class User {
    private int id;
    private String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() { return this.id; }
    public String getName() { return this.name; }
}

List<User> users = Arrays.asList(
    new User(1, "Alice"),
    new User(2, "Bob"),
    new User(3, "Charlie")
);

Map<Integer, String> userMap = users
    .stream()
    .collect(Collectors.toMap(User::getId, User::getName));

// 結果: {1=Alice, 2=Bob, 3=Charlie}

メソッド参照(User::getIdなど)を使うとすっきり書けます。ラムダ式でu -> u.getId()と書いても同じ意味です。

重複キーがある場合

リストに同じキーになる要素が2件以上あるとIllegalStateExceptionが発生します。3番目の引数(マージ関数)を渡すことで対処できます。

Java
Map<Integer, String> userMap = users
    .stream()
    .collect(Collectors.toMap(
        User::getId,
        User::getName,
        (existing, replacement) -> existing  // 既存の値を優先する場合
    ));

⚠️ 注意: valueにnullが入るとNullPointerExceptionになります。nullを含む可能性があるリストはあらかじめfilterでnullを弾いておくか、後述のHashMap::newを使う方法に切り替えるのが無難です。

MapのキーをListへ変換する

keySet()をそのまま使う

一番シンプルな方法です。変換だけしたい場合はこれで十分です。

Java
Map<Integer, String> map = Map.of(1, "Alice", 2, "Bob", 3, "Charlie");
List<Integer> keyList = new ArrayList<>(map.keySet());

Map.of()はJava 9以降で使えます。Java 8環境では使えないので注意が必要です。Java 8ではnew HashMap<>()に値を個別にput()するか、後述のStreamで組み立てる形になります。

Streamを使う

Streamを挟むとソートや絞り込みと組み合わせやすくなります。

Java
// Listに変換するだけ
List<Integer> keyList = map
    .keySet()
    .stream()
    .collect(Collectors.toList());

// ソートして取り出す
List<Integer> sortedKeys = map
    .keySet()
    .stream()
    .sorted()
    .collect(Collectors.toList());

💡 補足: Map.keySet()が返すSetは順序を保証しません。挿入順を保ちたい場合はLinkedHashMapを使うかsorted()を挟むと判断できるわけですね。

よく使うStream処理のパターン

filter — 条件を絞り込む

条件に合う要素だけを残したいときに使います。

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

List<Integer> evens = numbers
    .stream()
    .filter(n -> (n % 2) == 0)
    .collect(Collectors.toList());

// 結果: [2, 4, 6]

Userリストから名前で絞り込む場合もよくあるパターンです。

Java
List<User> filtered = users
    .stream()
    .filter(u -> u.getName().startsWith("A"))
    .collect(Collectors.toList());

// 結果: Alice だけ残る

map — 要素を変換する

Streamのmapは「各要素を別の値に変換する」操作です。コレクションのMap(Key-Value型)とは別物なので最初は少し紛らわしいですが、役割はまったく違うと考えるとわかりやすいです。

Java
List<String> names = List.of("alice", "bob", "charlie");

List<String> upper = names
    .stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());

// 結果: [ALICE, BOB, CHARLIE]

数値に変換して合計を出したい場合はmapToInt()が便利です。

Java
int totalId = users
    .stream()
    .mapToInt(User::getId)
    .sum();

Streamの条件式や変換処理を追いかけるなら、Eclipseの条件付きブレークポイントも覚えておくと便利です。

flatMap — ネストしたListを平坦化する

リストのリスト(ネスト構造)を1つのリストにまとめたいときに使います。

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

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

// 結果: [1, 2, 3, 4, 5, 6]

mapだとListのListのままになってしまいますがflatMapを使うと1段階平坦化できます。

distinct / sorted — 重複除去・ソート

Java
List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6, 5);

List<Integer> result = nums
    .stream()
    .distinct()   // 重複を除く
    .sorted()     // 昇順にソート
    .collect(Collectors.toList());

// 結果: [1, 2, 3, 4, 5, 6, 9]

降順にしたい場合はsorted(Comparator.reverseOrder())を使います。

groupingBy — グループ化してMapに変換する

toMap()は1対1のマッピングですが同じキーに複数の要素を紐づけたい(1対多)場合はgroupingByが向いています。

Java
// 部署IDでグループ化(ここではidをキーにした例)
Map<Integer, List<User>> grouped = users
    .stream()
    .collect(Collectors.groupingBy(User::getId));

// グループごとの件数が欲しい場合
Map<Integer, Long> countById = users
    .stream()
    .collect(Collectors.groupingBy(User::getId, Collectors.counting()));

toMapは「1つのキーに1つの値」、groupingByは「1つのキーに複数の値」というイメージで使い分けると迷いにくいです。

joining — 文字列を結合する

Java
List<String> words = List.of("Java", "Stream", "API");

// 区切り文字だけ
String joined = words
    .stream()
    .collect(Collectors.joining(", "));

// 結果: "Java, Stream, API"

// 前置・後置を付ける場合
String wrapped = words
    .stream()
    .collect(Collectors.joining(", ", "[", "]"));

// 結果: "[Java, Stream, API]"

count / findFirst / anyMatch

Java
// 条件に合う件数
long count = users
    .stream()
    .filter(u -> u.getName().startsWith("A"))
    .count();

// 条件に合う最初の1件(Optionalで返ってくる)
Optional<User> first = users
    .stream()
    .filter(u -> u.getId() > 1)
    .findFirst();

// 条件を満たす要素が1件でもあるか
boolean hasAlice = users
    .stream()
    .anyMatch(u -> u.getName().equals("Alice"));

// 全件が条件を満たすか
boolean allPositive = numbers
    .stream()
    .allMatch(n -> n > 0);

// 条件を満たす要素が1件もないか
boolean noneNegative = numbers
    .stream()
    .noneMatch(n -> n < 0);

findFirst()Optional<T>を返すので.orElse(null).orElseThrow()などで値を取り出します。

Collectors早見表

Collector主な用途
toList()Listに変換する。
toUnmodifiableList()変更不可のListに変換する(Java 10以降)。
toSet()Setに変換する(重複が自動で除去される)。
toMap(k, v)Mapに変換する(1対1)。
groupingBy(f)グループ化して Map<K, List<V>> に変換する(1対多)。
joining(区切り)文字列を結合する。
counting()groupingBy との組み合わせでグループ内件数を集計する。
toUnmodifiableMap(k, v)変更不可のMapに変換する(Java 10以降)。
partitioningBy(predicate)条件でtrueとfalseの2グループに分ける。

Q&A

Stream.toList()とCollectors.toList()の違いは?

Java 16以降はstream.toList()と短く書けるようになりました。ただしこちらは変更不可(Unmodifiable)なListを返します。add()remove()を後から呼ぶとUnsupportedOperationExceptionになるので注意が必要です。追加・削除が必要な場合はCollectors.toList()new ArrayList<>(stream.toList())で対処できますよ。

toMap()でnullがあるとエラーになる?

Collectors.toMap()はvalueがnullだとNullPointerExceptionが発生します。回避策の一つはマップファクトリとしてHashMap::new を明示する方法です。

Java
Map<Integer, String> map = users
    .stream()
    .collect(Collectors.toMap(
        User::getId,
        u -> u.getName(),  // null になりうるvalue
        (a, b) -> a,
        HashMap::new       // nullを許容するHashMapを使う
    ));
Streamは一度使ったら再利用できない?

そうです。一度collect()forEach()など終端操作を呼び出すとそのStreamはもう使えなくなります。再度処理したい場合はlist.stream()から作り直す必要があります。「使い捨て」のイメージで覚えておくとわかりやすいです。

parallelStream() はいつ使うべきですか?

stream() の代わりにparallelStream()を使うと処理が並列化されます。大量データの重い処理には効果的ですが、スレッドセーフでない操作(例: 外部カウンタのインクリメント)を混ぜるとバグの原因になります。業務でDB検索結果のListを処理する程度であれば通常のstream()で十分なことがほとんどです。

Java開発まわりの設定やデバッグもあわせて確認したい方は、Eclipse・Java関連の記事まとめもどうぞ。

まとめ

  • ListからMapへの変換はCollectors.toMap(keyMapper, valueMapper)が基本だよ。
  • 重複キーがあるときは第3引数のマージ関数を忘れずに渡してね。
  • MapのキーをListにするだけならnew ArrayList<>(map.keySet())が一番シンプルだよ。
  • Streamは中間操作(filter / map / sortedなど)を繋げて終端操作(collect / count / findFirstなど)で結果を出す流れだよ。
  • groupingByは1対多のグループ化、toMap は1対1のマッピングと使い分けてね。
  • stream.toList()(Java 16〜)は変更不可のListを返すので後から追加・削除が必要な場合は Collectors.toList() を使ってね。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次