JavaのコレクションとStream APIの基本を初心者向けにわかりやすく解説
Javaを勉強していると、List、Set、Map といったコレクションがよく出てきます。
例えば、複数の名前をまとめて扱いたい場合、以下のように List を使えます。
List<String> names = new ArrayList<>();
names.add("田中");
names.add("佐藤");
names.add("鈴木");
System.out.println(names);
また、Javaではコレクションに対して Stream API を使うことで、絞り込み、変換、集計などの処理を分かりやすく書くことができます。
List<String> names = List.of("田中", "佐藤", "鈴木", "佐々木");
List<String> result = names.stream()
.filter(name -> name.startsWith("佐"))
.toList();
System.out.println(result);
ただ、Javaを学び始めたばかりの頃は、以下のような点で迷いやすいです。
- List、Set、Map の違いが分からない
- ArrayList と HashSet の使い分けが分からない
- Map はコレクションなのか分からない
- stream() が使えるもの、使えないものが分からない
- Stream API の filter や map の使い方が分からない
この記事では、Javaのコレクションと Stream API について、基本的な考え方から実務でよく使う書き方までまとめます。
- 1. Javaのコレクションとは
- 2. 代表的なコレクション
- 3. Listとは
- 4. Setとは
- 5. Mapとは
- 6. List、Set、Mapの使い分け
- 7. 変数の型はインターフェースで宣言する
- 8. Stream APIとは
- 9. Stream APIの基本形
- 10. 中間操作と終端操作
- 11. filterの使い方
- 12. mapの使い方
- 13. filterとmapを組み合わせる
- 14. forEachの使い方
- 15. sortedの使い方
- 16. distinctの使い方
- 17. countの使い方
- 18. anyMatch、allMatch、noneMatchの使い方
- 19. findFirstの使い方
- 20. collectの使い方
- 21. toMapの使い方
- 22. groupingByの使い方
- 23. MapでStream APIを使う場合
- 24. Stream APIで注意すること
- 25. 実務でよく使うStream APIの例
- 26. まとめ
Javaのコレクションとは
Javaのコレクションとは、複数のデータをまとめて扱うための仕組みです。
例えば、複数の文字列を扱いたい場合、配列を使うこともできます。
String[] names = new String[3];
names[0] = "田中";
names[1] = "佐藤";
names[2] = "鈴木";
ただし、配列は最初に要素数を決める必要があります。
一方、コレクションを使うと、あとから要素を追加したり削除したりしやすくなります。
List<String> names = new ArrayList<>();
names.add("田中");
names.add("佐藤");
names.add("鈴木");
names.add("山田");
業務アプリケーションでは、DBから取得した複数件のデータを扱うことが多いため、配列よりもコレクションを使う場面が多いです。
代表的なコレクション
Javaでよく使う代表的なコレクションは、以下の3つです。
- List・・・順番あり、重複OK
- Set・・・重複NG
- Map・・・キーと値のペアで管理する
まずは、List、Set、Map の違いを押さえることが大切です。
Listとは
List は、順番を持ったデータの集まりです。
追加した順番を保持し、同じ値を複数入れることもできます。
List<String> names = new ArrayList<>();
names.add("田中");
names.add("佐藤");
names.add("田中");
System.out.println(names);
実行結果は以下のようになります。
[田中, 佐藤, 田中]
田中を2回追加していますが、List では重複が許されるため、そのまま保持されます。
Listを使う場面
List は、実務でも非常によく使います。
例えば、DBからユーザー一覧を取得する場合です。
List<User> users = userRepository.findAll();
複数件のデータを順番付きで扱いたい場合は、まず List を使うと考えると分かりやすいです。
Setとは
Set は、重複を許さないデータの集まりです。
Set<String> names = new HashSet<>();
names.add("田中");
names.add("佐藤");
names.add("田中");
System.out.println(names);
田中を2回追加していますが、Set では重複が除かれます。
ただし、HashSet は基本的に順番を保証しません。
Setを使う場面
Set は、重複をなくしたい場合に使います。
Set<String> userIds = new HashSet<>();
userIds.add("user001");
userIds.add("user002");
userIds.add("user001");
System.out.println(userIds);
同じユーザーIDを複数持ちたくない場合などに便利です。
Mapとは
Map は、キーと値のペアでデータを管理する仕組みです。
List や Set と違い、Map は Collection インターフェースを直接継承していません。
ただし、Javaで複数データを扱う代表的な仕組みとして、List、Set と一緒に説明されることが多いです。
Map<String, String> userMap = new HashMap<>();
userMap.put("user001", "田中");
userMap.put("user002", "佐藤");
System.out.println(userMap.get("user001"));
この例では、user001 というキーに対して、田中という値を紐づけています。
Mapを使う場面
Map は、キーを使って値を取り出したい場合に使います。
例えば、ユーザーIDをキーにして、ユーザー情報を取得したい場合です。
Map<String, User> userMap = new HashMap<>();
User user = userMap.get("user001");
キーを使って素早く値を取り出したい場合に Map を使います。
List、Set、Mapの使い分け
List、Set、Map の使い分けは、まず以下のように考えると分かりやすいです。
- 順番付きで複数件を扱いたい・・・List
- 重複をなくしたい・・・Set
- キーを使って値を取り出したい・・・Map
実務では、DB検索結果のような複数件データには List を使うことが多いです。
一方で、重複排除をしたい場合は Set、IDやコードをキーにして値を取り出したい場合は Map を使います。
変数の型はインターフェースで宣言する
Javaでは、以下のように左側を List、右側を ArrayList として書くことが多いです。
List<String> names = new ArrayList<>();
左側の List はインターフェースです。
右側の ArrayList は具体的な実装クラスです。
以下のように書くこともできます。
ArrayList<String> names = new ArrayList<>();
ただし、実務では次のようにインターフェースで宣言することが多いです。
List<String> names = new ArrayList<>();
このように書くことで、あとから実装クラスを変更しやすくなります。
Stream APIとは
Stream API は、コレクションの要素に対して、絞り込み、変換、集計などを行うための仕組みです。
例えば、List の中から特定の条件に合う要素だけを取り出す場合、以下のように書けます。
List<String> names = List.of("田中", "佐藤", "鈴木", "佐々木");
List<String> result = names.stream()
.filter(name -> name.startsWith("佐"))
.toList();
System.out.println(result);
実行結果は以下です。
[佐藤, 佐々木]
この例では、名前が 佐 から始まるものだけを抽出しています。
Stream APIの基本形
Stream API の基本形は以下です。
コレクション.stream()
.中間操作()
.終端操作();
例えば、以下のような流れです。
- stream() で Stream を作る
- filter() や map() で加工する
- toList() や count() で結果を作る
実際のコードで見ると、以下のようになります。
List<String> result = names.stream()
.filter(name -> name.startsWith("佐"))
.toList();
filter は中間操作、toList は終端操作です。
中間操作と終端操作
Stream API では、処理が大きく中間操作と終端操作に分かれます。
- 中間操作・・・Streamを加工する処理
- 終端操作・・・最終的な結果を作る処理
代表的な中間操作には、filter、map、sorted、distinct などがあります。
代表的な終端操作には、toList、forEach、count、collect などがあります。
注意点として、Stream API は終端操作が呼ばれるまで実際の処理が実行されません。
names.stream()
.filter(name -> {
System.out.println(name);
return name.startsWith("佐");
});
このコードは、終端操作がないため、実際には処理が実行されません。
以下のように toList などの終端操作を付ける必要があります。
names.stream()
.filter(name -> {
System.out.println(name);
return name.startsWith("佐");
})
.toList();
filterの使い方
filter は、条件に合う要素だけを残す処理です。
List<String> names = List.of("田中", "佐藤", "鈴木", "佐々木");
List<String> result = names.stream()
.filter(name -> name.startsWith("佐"))
.toList();
System.out.println(result);
filter の中には、true または false を返す条件を書きます。
この例では、name.startsWith(“佐") が true になる要素だけが残ります。
mapの使い方
map は、要素を別の値に変換する処理です。
例えば、文字列を大文字に変換する場合は以下のように書けます。
List<String> names = List.of("tanaka", "sato", "suzuki");
List<String> result = names.stream()
.map(name -> name.toUpperCase())
.toList();
System.out.println(result);
実行結果は以下です。
[TANAKA, SATO, SUZUKI]
また、User オブジェクトのリストから名前だけを取り出す場合にも map を使えます。
List<String> names = users.stream()
.map(user -> user.getName())
.toList();
メソッド参照を使うと、以下のようにも書けます。
List<String> names = users.stream()
.map(User::getName)
.toList();
filterとmapを組み合わせる
Stream API では、filter や map をつなげて書くことができます。
例えば、20歳以上のユーザーだけに絞り込み、そのユーザー名だけを取り出す場合です。
List<String> adultNames = users.stream()
.filter(user -> user.getAge() >= 20)
.map(User::getName)
.toList();
この処理は、以下のような流れです。
- ユーザー一覧をStreamにする
- 20歳以上のユーザーだけに絞り込む
- Userから名前だけを取り出す
- Listに変換する
実務でも、このような filter と map の組み合わせはよく使います。
forEachの使い方
forEach は、要素を1つずつ処理する終端操作です。
List<String> names = List.of("田中", "佐藤", "鈴木");
names.stream()
.forEach(name -> System.out.println(name));
メソッド参照を使うと、以下のようにも書けます。
names.stream()
.forEach(System.out::println);
ただし、単純にループするだけであれば、拡張for文の方が分かりやすい場合もあります。
for (String name : names) {
System.out.println(name);
}
Stream API は便利ですが、何でも stream() で書けばよいというわけではありません。
sortedの使い方
sorted は、要素を並び替える中間操作です。
List<Integer> numbers = List.of(3, 1, 2);
List<Integer> result = numbers.stream()
.sorted()
.toList();
System.out.println(result);
実行結果は以下です。
[1, 2, 3]
オブジェクトの項目で並び替えたい場合は、Comparator を使います。
List<User> result = users.stream()
.sorted(Comparator.comparing(User::getAge))
.toList();
降順にしたい場合は、reversed を使います。
List<User> result = users.stream()
.sorted(Comparator.comparing(User::getAge).reversed())
.toList();
distinctの使い方
distinct は、重複を取り除く中間操作です。
List<String> names = List.of("田中", "佐藤", "田中", "鈴木");
List<String> result = names.stream()
.distinct()
.toList();
System.out.println(result);
実行結果は以下です。
[田中, 佐藤, 鈴木]
独自クラスで distinct を使う場合は、equals と hashCode が適切に実装されている必要があります。
countの使い方
count は、要素数を数える終端操作です。
List<String> names = List.of("田中", "佐藤", "鈴木", "佐々木");
long count = names.stream()
.filter(name -> name.startsWith("佐"))
.count();
System.out.println(count);
この例では、佐から始まる名前の件数を数えています。
anyMatch、allMatch、noneMatchの使い方
条件に一致する要素があるか確認したい場合は、anyMatch、allMatch、noneMatch を使います。
anyMatch は、1つでも条件に合う要素があるかを確認します。
boolean exists = users.stream()
.anyMatch(user -> user.getAge() >= 20);
allMatch は、全ての要素が条件に合うかを確認します。
boolean allAdult = users.stream()
.allMatch(user -> user.getAge() >= 20);
noneMatch は、条件に合う要素が1つもないかを確認します。
boolean noAdult = users.stream()
.noneMatch(user -> user.getAge() >= 20);
findFirstの使い方
findFirst は、条件に合う最初の要素を取得する終端操作です。
Optional<User> user = users.stream()
.filter(u -> u.getAge() >= 20)
.findFirst();
findFirst の戻り値は Optional です。
条件に合う要素が存在しない可能性があるためです。
値が存在する場合だけ処理したい場合は、以下のように書けます。
users.stream()
.filter(u -> u.getAge() >= 20)
.findFirst()
.ifPresent(user -> System.out.println(user.getName()));
collectの使い方
collect は、Stream の結果を List、Set、Map などに変換するときに使います。
Java 16以降で List に変換するだけなら、toList を使えます。
List<String> names = users.stream()
.map(User::getName)
.toList();
Java 8などでは、Collectors.toList を使うことが多いです。
List<String> names = users.stream()
.map(User::getName)
.collect(Collectors.toList());
Set に変換したい場合は、Collectors.toSet を使います。
Set<String> names = users.stream()
.map(User::getName)
.collect(Collectors.toSet());
toMapの使い方
Stream の結果を Map に変換したい場合は、Collectors.toMap を使います。
例えば、ユーザーIDをキーにして、User オブジェクトを値にする場合は以下です。
Map<String, User> userMap = users.stream()
.collect(Collectors.toMap(
User::getId,
user -> user
));
この書き方は実務でもよく使います。
ただし、キーが重複すると例外になるため注意が必要です。
重複時の扱いを指定する場合は、以下のように書きます。
Map<String, User> userMap = users.stream()
.collect(Collectors.toMap(
User::getId,
user -> user,
(oldValue, newValue) -> oldValue
));
この例では、同じキーが存在した場合、古い値を残します。
groupingByの使い方
groupingBy は、条件ごとにグループ化したい場合に使います。
例えば、ステータスごとにユーザーをまとめる場合です。
Map<String, List<User>> usersByStatus = users.stream()
.collect(Collectors.groupingBy(User::getStatus));
このように書くと、ステータスごとにユーザー一覧をまとめた Map が作られます。
部署別、カテゴリ別、ステータス別などでデータをまとめたい場合に便利です。
MapでStream APIを使う場合
List や Set は、直接 stream() を使えます。
list.stream();
set.stream();
一方で、Map は直接 stream() を使えません。
map.stream(); // コンパイルエラー
Map に対して Stream API を使いたい場合は、keySet、values、entrySet のいずれかを使います。
キーをStreamで処理する
Map のキーを処理したい場合は、keySet を使います。
map.keySet().stream()
.forEach(System.out::println);
値をStreamで処理する
Map の値を処理したい場合は、values を使います。
map.values().stream()
.forEach(System.out::println);
キーと値のペアをStreamで処理する
キーと値の両方を使いたい場合は、entrySet を使います。
map.entrySet().stream()
.forEach(entry -> {
System.out.println(entry.getKey());
System.out.println(entry.getValue());
});
実務では、Map を Stream API で扱う場合、entrySet を使うことが多いです。
Stream APIで注意すること
Stream API は便利ですが、いくつか注意点があります。
元のコレクションは変更されない
filter や map を使っても、基本的に元のコレクションは変更されません。
List<String> names = List.of("tanaka", "sato");
List<String> upperNames = names.stream()
.map(String::toUpperCase)
.toList();
System.out.println(names);
System.out.println(upperNames);
元の names はそのままで、変換後の結果が upperNames に入ります。
Streamは再利用できない
Stream は、一度終端操作を実行すると再利用できません。
Stream<String> stream = names.stream();
stream.count();
stream.toList(); // エラー
必要な場合は、もう一度 stream() を呼び出します。
long count = names.stream().count();
List<String> list = names.stream().toList();
nullに注意する
Stream API を使う場合も、null には注意が必要です。
List<String> names = Arrays.asList("田中", null, "佐藤");
List<String> result = names.stream()
.map(String::toUpperCase)
.toList();
このコードは、null に対して toUpperCase を呼び出そうとして NullPointerException になります。
null を除外してから処理する場合は、以下のように書けます。
List<String> result = names.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.toList();
forEachで外部の変数を変更しすぎない
Stream API の中で、外部の変数を変更しすぎると処理が分かりにくくなります。
例えば、以下のような書き方です。
List<String> result = new ArrayList<>();
users.stream()
.filter(user -> user.getAge() >= 20)
.forEach(user -> result.add(user.getName()));
この処理は動きますが、Stream の外側の変数を変更しているため、少し読みにくいです。
この場合は、以下のように map と toList を使う方が自然です。
List<String> result = users.stream()
.filter(user -> user.getAge() >= 20)
.map(User::getName)
.toList();
複雑な処理は無理にStreamにしない
Stream API は、絞り込み、変換、集計のような処理には向いています。
一方で、条件分岐が多い処理や、途中で複数の値を更新するような処理は、通常のfor文の方が分かりやすい場合があります。
for (User user : users) {
if (user.getAge() >= 20) {
System.out.println(user.getName());
}
}
Stream API は便利ですが、読みやすさを優先して使うことが大切です。
実務でよく使うStream APIの例
最後に、実務でよく使う Stream API の例を紹介します。
IDの一覧を作る
List<String> userIds = users.stream()
.map(User::getId)
.toList();
有効なユーザーだけ取得する
List<User> activeUsers = users.stream()
.filter(User::isActive)
.toList();
IDをキーにしたMapを作る
Map<String, User> userMap = users.stream()
.collect(Collectors.toMap(
User::getId,
user -> user
));
ステータスごとにグループ化する
Map<String, List<User>> usersByStatus = users.stream()
.collect(Collectors.groupingBy(User::getStatus));
条件に合うデータが存在するか確認する
boolean exists = users.stream()
.anyMatch(user -> user.getAge() >= 20);
まとめ
Javaのコレクションは、複数のデータをまとめて扱うための仕組みです。
代表的なものとして、List、Set、Map があります。
- List・・・順番あり、重複OK
- Set・・・重複NG
- Map・・・キーと値のペアで管理する
また、Stream API を使うことで、コレクションに対する絞り込み、変換、集計などの処理を分かりやすく書けます。
- filter・・・条件に合う要素だけ残す
- map・・・要素を別の値に変換する
- sorted・・・並び替える
- distinct・・・重複を除く
- count・・・件数を数える
- toList・・・Listに変換する
- collect・・・List、Set、Mapなどに変換する
List や Set は直接 stream() を使えます。
一方で、Map は直接 stream() を使えないため、keySet、values、entrySet を経由して Stream API を使います。
Stream API は便利ですが、複雑な処理を無理に1行で書こうとすると、かえって読みにくくなることがあります。
まずは、filter、map、toList の基本形から覚えて、読みやすさを意識して使うのがおすすめです。


