Java Stream APIの使い方|List・Map変換とよく使う処理まとめ【Java 8〜17対応】

当ページのリンクには広告が含まれています。
「Java Stream API 基本から実践まで完全解説」と書かれた記事のアイキャッチ

ListからMapへ変換するならCollectors.toMap()が基本です。

たとえばUser::getIdをキーUser::getNameを値にする場合はusers.stream().collect(Collectors.toMap(User::getId, User::getName))と書けます。

逆にMapのキーをListへ変換するだけならnew ArrayList<>(map.keySet())がシンプルです。この記事では、この2つを起点によく使うStream処理をまとめています。

目次

Java 8〜17での書き方の違いと注意点

API・書き方利用できるバージョン
Arrays.asList()Java 8で利用可能
List.of()・Map.of()Java 9以降
Collectors.toUnmodifiableList()・toUnmodifiableMap()Java 10以降
Stream.toList()Java 16以降

Java 8が今も更新される理由やLTSごとのサポート状況はJava 8とJava 26の違いを整理した記事で確認できます。

for文 vs Stream

比較Stream APIfor文
可読性絞り込みや変換を連続して書く処理に向く単純なループや分岐が多い処理に向く
実行性能処理内容による。逐次Streamが常に速いわけではない単純処理ではオーバーヘッドを抑えやすい
デバッグパイプラインの途中状態を追いにくい1ステップずつ追いやすい
並列化parallelStream()へ切り替えやすいが、必ず高速になるわけではないExecutorやFork/Joinなど別の仕組みが必要

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  // 既存の値を優先する場合
    ));

valueMapperがnullを返すとNullPointerExceptionが発生します。mapFactoryへHashMap::newを指定しても回避できません。nullが不要なら変換前にfilterで除外し、nullをMapへ残す場合はHashMap#put()など別の方法で格納します。

MapのキーをListへ変換する

keySet()をそのまま使う

keySet()の反復順序は元のMap実装に従います。HashMapは順序を保証しません。挿入順を保つならLinkedHashMap、キー順に並べ替えるならStreamのsorted()を使います。

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ではnew HashMap<>()でMapを作成しput()でキーと値を追加します。

Streamを使う

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

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

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

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

filter — 条件を絞り込む

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

Java
List<Integer> numbers = Arrays.asList(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 = Arrays.asList("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 = Arrays.asList(
    Arrays.asList(1, 2, 3),
    Arrays.asList(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 = Arrays.asList(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
List<String> names = Arrays.asList("Alice", "Andrew", "Bob");

Map<Character, List<String>> grouped = names
    .stream()
    .collect(Collectors.groupingBy(name -> name.charAt(0)));

// 結果: {A=[Alice, Andrew], B=[Bob]}

Map<Character, Long> countByInitial = names
    .stream()
    .collect(Collectors.groupingBy(
        name -> name.charAt(0),
        Collectors.counting()
    ));

// 結果: {A=2, B=1}

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

joining — 文字列を結合する

Java
List<String> words = Arrays.asList("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グループに分ける。

よくある質問

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

Java 16以降ではstream.toList()と短く書けますが返されるListは変更できません。add()やremove()を呼ぶとUnsupportedOperationExceptionが発生します。

Collectors.toList()はAPI仕様上変更可能なListを返すとは保証されていません。後から追加・削除する前提なら、次のようにArrayListを明示しておくと確実です。

Java
List<User> users = stream.collect(Collectors.toCollection(ArrayList::new));

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

Collectors.toMap()でvalueがnullになるとNullPointerExceptionが発生します。

HashMap自体はnullを保持できますがCollectors.toMap()のmapFactoryへHashMap::newを指定するだけでは回避できません。

nullが不要なら変換前に除外するのがシンプルです。

Java
Map<Integer, String> userMap = users
    .stream()
    .filter(user -> user.getName() != null)
    .collect(Collectors.toMap(User::getId, User::getName));

nullをMapへ残す必要がある場合はCollectors.toMap()にこだわらずHashMap#put()で格納するほうが分かりやすいです。

Streamは一度使ったら再利用できない?

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

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

parallelStream()は処理を並列化しますが、それだけで逐次Streamより速くなるとは限りません。データ量、処理コスト、CPUコア数、順序維持の有無などで結果が変わるため、対象データを使って計測してから採用します。外部の可変状態を更新する処理は、競合や結果の不定につながるため避けてください。


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は変更できません。追加・削除できるArrayListが必要な場合は、collect(Collectors.toCollection(ArrayList::new))を使います。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次