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 について、基本的な考え方から実務でよく使う書き方までまとめます。

スポンサーリンク

※このページにはプロモーションが含まれています。当サイトは各種アフィリエイトプログラムから一定の収益を得ています。

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 の基本形から覚えて、読みやすさを意識して使うのがおすすめです。

スポンサーリンク

Java