Java

Stream APIの書き方簡易まとめ

More than 1 year has passed since last update.

わりと今さら感ありますが、Java8を使い始めたので復習がてらまとめてみました。
もし書いてあることが間違ってたら、ご指摘頂けると助かります...

Stream APIとは

Java8より導入された、配列やCollection等データの集合に対する処理を書くことができるAPI。
処理はメソッドチェーンで定義していく形になり、処理の区分としては大きく中間操作・終端操作があります。

中間操作

対象の集合に対して、値のフィルタリングやソート・値の加工といったことができます。
中間操作は組み合わせて実行すること、省略することも可能。

終端操作

中間操作を行ったストリームより値の集計、最大値、最小値の取得といったことができます。
1度のみ実行可能。

実際どんな感じで書くのか

オブジェクトの集合を扱う場合

ListからStreamを生成する場合
List<String> wordList = new ArrayList<>();
wordList.add("one");
wordList.add("two");
wordList.add("three");
Stream<String> wordListStream = wordList.stream();

// 昇順にソートして、ソート後の値を表示する。
wordListStream.sorted(Comparator.naturalOrder())
  .forEach(System.out::println);
/* 出力結果
one
three
two
*/
配列からStreamを生成する場合
String[] wordArray = {"one", "two", "three"};
Stream<String> wordArrayStream = Arrays.stream(wordArray);

// 降順にソートして、Map<String, Integer>を生成する。
Map<String, Integer> wordMap =
  wordArrayStream.sorted(Comparator.reverseOrder())
    .collect(Collectors.toMap(v -> v, v -> v.length(), (v1, v2) -> v1, LinkedHashMap::new));

wordMap.forEach((s, i) -> System.out.printf("%s=%d\n", s, i));
/* 出力結果
two=3
three=5
one=3
*/

Streamクラスから生成する場合
Stream<String> stream = Stream.of("test1", "test22", "test333");
// 終端操作で文字列長が一番長い文字列を取得する。
Optional<String> maxLengthString =
  stream.map(v -> "mapped_" + v).max((v1, v2) -> Integer.compare(v1.length(), v2.length()));

System.out.println(maxLengthString.get());
/* 出力結果
test333
*/
MapからStreamを生成する場合
// Mapからは直接Streamを生成できないため、1度Map.Entryの形式に変換する必要がある。
Stream<Map.Entry<String, Integer>> mapStream = wordMap.entrySet().stream();
自前のクラスの集合をStreamで扱う場合
public class Hoge {
  private int i;
  private int j;

  Hoge(int i, int j) {
    this.i = i;
    this.j = j;
  }

  public int getI() { return this.i; }
  public int getJ() { return this.j; }
}

public void doSomething() {
  List<Hoge> hogeList = new ArrayList<>();
  hogeList.add(new Hoge(2, 2));
  hogeList.add(new Hoge(4, 1));
  hogeList.add(new Hoge(1, 3));

  // クラスHogeのフィールドiの値を基準に昇順にソートし、フィールドiの要素のみ取得して表示する。
  hogeList.stream().sorted(Comparator.comparingInt(Hoge::getI))
    .mapToInt(Hoge::getI).forEach(System.out::println);
  /* 出力結果
  1
  2
  4
  */
}

Collecton,Arraysといったインターフェースに追加されたstreamメソッドを実行するか、Streamインターフェースに定義されたofメソッドを利用することでStreamを生成することができます。
簡単に、中間操作・終端操作で利用しているメソッドの紹介をします。

中間操作

  • sorted
    • 名前の通り、集合に含まれるオブジェクトをソートする。Comparableインターフェースが実装されているクラスであれば、その実装を元にソートしてくれるようです。
    • Comparatorインターフェースの形式で処理を引数として渡すことで、ソート方法が指定できます。
      • Comparableインターフェースの実装に頼る場合は、すでに用意されているものを利用することができます。
        • Comparator.naturalOrder ... 昇順でソート。
        • Comparator.reverseOrder ... 降順でソート。
      • Comparableインターフェースの実装に頼らない場合は、Comparator.comparingIntといった用意されているメソッドを利用するか、Comparatorインターフェースを自前で実装するかで対応できます。
  • map
    • オブジェクト1つ1つの内容を変更することが可能。別の型の値や配列に変換することも可。Functionインターフェースの形式で変換処理を渡す必要があります。
  • mapToInt
    • プリミティブな値の集合を扱う場合は、専用のStreamを生成する必要があります(intの場合はIntStream)。mapToIntは通常のStreamからIntStreamを生成します。
    • ToIntFunctionインターフェースの形式で、オブジェクトをint型に変換する処理を渡してやる必要があります。

終端操作

  • forEach
    • Consumerインターフェースの形式で実装した処理を、集合のオブジェクト1つ1つを用いて行います。
  • collect
    • StreamをMap,Listといった集合に変換することができます。
    • Collectorインターフェースの形式で処理を渡してやる必要があります。
  • max
    • Comparatorインターフェースの実装に基づいてソートし、最大値となるオブジェクトを取得することができます。なお、取得する値はOptional型にラップされた状態で受け取ることになります。

プリミティブな値の集合を扱う場合

プリミティブ型向けに用意されているStreamを利用することになります。

ただし、用意されているのはIntStream,LongStream,DoubleStreamのみ。
それ以外のプリミティブ型はボクシングした状態で扱うしかなさそうですね。

プリミティブ用Streamを扱う場合
int result1 = IntStream.range(1, 3).sum();
System.out.println(result1);
// 出力結果: 3

long result2 = IntStream.rangeClosed(1, 10).filter(i -> (i % 2) == 0).count();
System.out.println(result2);
// 出力結果: 5

OptionalLong result3 = LongStream.iterate(1, l -> l + 1).limit(5).reduce((l, l2) -> l * l2);
System.out.println(result3.getAsLong());
// 出力結果: 120

中間操作

  • range,rangeClosed
    • 値の範囲を指定して生成することができます。
      • rangeは第2引数に指定した値より1つ前の値まで生成。
      • rangeClosedは第2引数に指定した値まで生成。
  • filter
    • 特定の条件を満たす値のみ抽出することができます。
  • iterate
    • 開始値と次の値を生成する処理をUnaryOperator(例ではLongUnaryOperator)に定義し、それを元に無限に値を生成することができます。
    • 無限に値を生成するため、limitといった要素数を制限する中間操作を入れる必要があります。
  • limit
    • 要素の始めから、指定した値の分だけ要素を取得することができます。

終端操作

  • sum
    • 集合の合計値を算出することができます。
  • count
    • 集合の要素数を算出することができます。
  • reduce
    • 集合の隣り合った要素同士に特定の処理を行わせ、最終的に1つの値に集約することができます。
      • すごく微妙な説明かもしれない...
      • 例では、すべての要素同士で乗算を行い結果を取得しています。

並列処理

parallelまたはparallelStreamといったメソッドを用いることで、並列処理可能なStreamが取得できます。
当然ながら、スレッドセーフを意識して実装しないと予期せぬ事態が起きてしまう可能性があります。

並列処理用ストリーム取得例
List<String> wordList = new ArrayList<>();
wordList.add("one");
wordList.add("two");
wordList.add("three");
Stream<String> parallelStream = wordList.parallelStream();

IntStream parallelIntStream = IntStream.range(1, 3).parallel();

Stream APIを使ってみて

中間操作の追加・変更・削除が簡単にできますし、統一された書き方が自然とできるようになるのでソースコードの保守性は上がりそうですね。
Java8の新機能を使うことで処理自体もだいぶ簡潔に書けますし、積極的に使っていきたいですね。