JavaのStream APIを使ってみよう

JavaのStream APIを使ってみよう

Javaの学習中に「〇〇.stream()」って出てきたけど何なの?
どういう場面で使われているの?
使い方は?

こんな疑問に答えていきます。

業務でJavaを使ってソースコードを書いている、エンジニア4年目です。
最近、「〇〇.stream()」という記述が出てきてどのような使い方なのか気になったのでまとめました。

実例を用いて、使い方をご紹介します。

Stream APIについて

JavaのStreamは、コレクションや配列などのデータを効率的に操作するためのAPIです。
Java 8で導入され、主にコレクションの要素を簡潔に処理するために使われます。
Streamはデータの流れを定義する抽象的なビューで、フィルタリング、マッピング、集約操作を行うことができます。

Streamは、「流れ」という意味があります。
川の流れのように、上流から下流にデータが流れていくイメージを持つと良いでしょう。
以下、Streamの主要な概念と使用例について説明します。

Streamの主な特徴

  • 非破壊性
    Streamを使用しても元のデータは変更されません。
  • 中間操作と終端操作
    中間操作は、新しいStreamを返し、チェーンの形でつなげることができます。
    例:filter(), map(), sorted()
    終端操作はStreamの処理を終了し、結果を得ます。
    例:forEach(), collect(), count()
  • 遅延実行
    中間操作はすぐに実行されず、終端操作が呼ばれたときに初めて処理が実行されます。これにより効率的な計算が可能です。

JavaのStream APIを使ってみよう

Stremを使うには、次の3ステップです。

  1. Streamの生成
  2. 中間操作
  3. 終端操作

それぞれ実例を用いて紹介していきます。

1. Streamの生成

まずは、Stremを生成しますが、その前に、StremはAPIですので、
最初にimportでStrem APIを使用できようにしておきましょう。

import java.util.stream.*;

Streamを生成します。生成方法はいくつかあります。
今回は、2つほど紹介します。

  • Java コレクションからStreamを生成する。
  • Stream.of()を使って直接ストリームを生成する

それぞれ具体例を見ていきます。

生成例① Java コレクションからStreamを生成する

import java.util.stream.*;
import java.util.List;
import java.util.Arrays;

List list = Arrays.asList("A", "B", "C");
// コレクションListからStreamの生成
Stream stream = list.stream();

生成例②:Stream.of()を使って直接ストリームを生成する

import java.util.stream.*;

Stream stream = Stream.of(1, 2, 3, 4, 5);

2. 中間操作

Stremを生成したら、次は中間操作です。
中間操作は、流れてきたデータにご希望に合わせて何かしらの処理を施します。
新しいStreamを返し、チェーンの形でつなげることができます。

処理の方法については、以下の例があります。

  • filter(Predicate): 要素を条件でフィルタリングします。
  • map(Function): 各要素に関数を適用して新しいStreamを生成します。
  • sorted(): 要素を順序付けます。

それぞれ、具体例を見ていきます。

具体例① : filter(Predicate): 要素を条件でフィルタリングします。

import java.util.stream.*;
import java.util.List;
import java.util.Arrays;

List names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println); // "Alice"

名前を格納したリストを作成しています。

List names = Arrays.asList("Alice", "Bob", "Charlie");

上記で作成したリスト内のデータをstream()によって、下流に流していきます。

names.stream()

上流から流れてきた、データにフィルターをかけます。ここでは、「A」から始まる名前をフィルタリングしています。

.filter(name -> name.startsWith("A"))

上流から流れてきたフィルタリングされたデータの結果を出力しています。ここでの結果は「Alice」のみ。

.forEach(System.out::println); // "Alice"

forEarch()は、次節で紹介する終端操作になります。ここでは、先に終端操作を紹介しています。

具体例② : map(Function): 各要素に関数を適用して新しいStreamを生成します。

import java.util.stream.*;
import java.util.List;
import java.util.Arrays;

List names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println); // "ALICE", "BOB", "CHARLIE"

名前を格納したリストを作成し、stream()によって、下流に流しています。

List names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()

上流から流れてきた、データに関数を適用します。ここでは、文字列を大文字に変換しています。

.map(String::toUpperCase)

上流から流れてきた関数が適用されたデータの結果を出力しています。
※次節で紹介する終端操作となります。

.forEach(System.out::println); // "ALICE", "BOB", "CHARLIE"

sorted(): 要素を順序付けます。

import java.util.stream.*;
import java.util.List;
import java.util.Arrays;

List numbers = Arrays.asList(5, 3, 8, 1);
numbers.stream()
.sorted()
.forEach(System.out::println); // 1, 3, 5, 8

3. 終端操作の例

終端操作はStreamの処理を終了し、結果を得ます。

処理の方法については、以下の例があります。

  • forEach(Consumer): 各要素に対して動作を行います。
  • collect(Collector): 結果をListやSetなどに集めます。
  • sorted(): 要素を順序付けます。

それぞれ、具体例を見ていきます。

forEach(Consumer): 各要素に対して動作を行います。

Stream stream = Stream.of("one", "two", "three");
stream.forEach(System.out::println);

collect(Collector): 結果をListやSetなどに集めます。

List names = Arrays.asList("Alice", "Bob", "Charlie");
List collectedNames = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());

count(): 要素数を返します。

List names = Arrays.asList("Alice", "Bob", "Charlie");
long count = names.stream()
.filter(name -> name.startsWith("A"))
.count(); // 1

4. 組み合わせの例

ここからは少し応用編です。

次の例では、リストの中の整数から偶数だけを取り出して、それぞれ2倍にし、結果をリストに集めます。

List numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List result = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.collect(Collectors.toList()); // [4, 8, 12]


パラレルストリーム
Streamは並列処理もサポートしています。parallelStream()を使うと、複数のスレッドで処理を並列に実行することが可能です。

List numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.parallelStream()
.map(n -> n * n)
.forEach(System.out::println);

並列ストリームは大規模データセットの処理で性能向上を期待できますが、全てのケースで効果的というわけではないので、スレッドのオーバーヘッドなどを考慮する必要があります。

まとめ

JavaのStreamは、コードの可読性を高め、シンプルで直感的にデータ処理を行える強力なツールです。中間操作と終端操作を組み合わせることで、データの操作を簡潔に表現することができ、並列処理によるパフォーマンス向上も可能です。