こんにちは、Sawa です。

前回のブログではラムダ式の書き方について学びました。
【Java】ラムダ式の書き方を理解しよう(Stream 理解への道3)

今回はいよいよ Stream API について学んでいこうと思います。 Stream は苦手意識を持つ方が多いですが、決して難しいものではありませんのでさっそく学んでいきましょう!

そもそも Stream とは?

ざっくり説明すると、リストなどのコレクションに対する処理(一部の条件に引っかかる要素のみを抽出、データの加工など)を簡潔に記述できるインタフェースです。
コレクションに対してStream API の持つメソッドを使うには一度 Stream の方に変換する必要があります。

import java.util.*;
import java.util.stream.Stream;

public class Main {

    public static void main(String[] args){
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        Stream<Integer> stream = list.stream();
    }
}

公式のドキュメントを見ると、コレクションは Stream 型を返す stream() を持っています。

インタフェースCollection<E>

この Stream に対して中間操作終端操作と呼ばれるものを行うことで、コレクションに対するデータ処理を行うことができます。 中間操作と終端操作って聞くと難しい印象を受けますが、大したことないです。
詳しく見ていきましょう。

中間操作

中間操作はデータの処理や抽出処理をする段階です。

主なメソッドの例を挙げます。

  • filter:条件にある要素のみに絞る。引数は Predicate
  • map:データを加工する。引数は Function

上記は中間操作のごく一部のメソッドです。
中間操作は引数として関数型インタフェースを受け取るものが多いです。

では、filter と map について説明していきます。

filterメソッド

filter は引数に Predicate 型を受け取り、Predicate で定義した条件に合う要素のみを Stream に要素として返します。

import java.util.*;
import java.util.stream.Stream;

public class Main {

    public static void main(String[] args){
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        Stream<Integer> stream = list.stream()
                .filter(e -> e > 3);
    }
}

Predicate が返す戻り値の型は boolean です。
そのため、このコードでは list の要素から値が4以上の要素のみを Stream の要素として返します。
ここで注意しいただきたいのが、終端処理が実行されていない段階では中間操作は実行されていないということです。
のちほど説明しますが、.filter().終端操作(); とすることで中間操作が実行されます。
ですので、この段階では filter 機能を持った Stream が作成されたというような状態です。

filter に渡している引数 e には list の要素を1つずつ渡しているので 1,2,3,4,5 と順番に渡されていきます。

そして Stream<T> の T に指定する型は list と同じ Integer です。

map メソッド

map は引数に Function 型を受け取り、List の持つ要素を加工して返します。

例を見てみましょう。

フィールドの id と name を持つ Book クラスをList の要素として追加し、その Book クラスの name のみを Stream の要素として返してみます。

だいぶ基礎的な書き方ですが、Book クラスを持つ List を定義して、name のみを抽出してみました。

import java.util.*;
import java.util.stream.Stream;

public class Main {

    public static void main(String[] args){
        List<Book> bookList = Arrays.asList(
                new Book(1, "Java 入門"),
                new Book(2, "Java 応用"),
                new Book(3, "Python 入門"));
        
        Stream<String> stream = bookList.stream()
                .map(e -> e.getName());
    }
}

class Book{
    int id;
    String name;
    
    Book(int id, String name){
        this.id = id;
        this.name = name;
    }
    
    String getName(){
        return this.name;
    }
}

map の引数 e は Book 型になります。

map の括弧の中身は Function の apply メソッドの中身になるので、戻り値として String 型の Book.getName() を返しています。 そして、String 型を返したので Stream<T> の T の部分に String を指定しています。

中間操作の使い方については理解できたでしょうか? 続いて終端操作について説明していきます。

終端操作

終端操作を行うことで、Stream をどのように活用するか決まってきます。
なお、中間操作は1つ の Stream に対して複数行えますが、終端操作は1つの Stream に対して1度だけです。

今回は特に理解しやすい終端操作をピックアップして紹介します。

  • count : 要素の数をカウントする。引数なし、戻り値は long 型
  • allMatch:要素の全てが指定した条件に合致するかを返す。引数は Predicate 型、戻り値は boolean 型
  • toList : Stream<T> をリストに変換する。引数なし、戻り値は List<T>

さっそく1つずつ見ていきましょう。

count メソッド

これは非常にシンプルです。 要素の数を数えるだけです。

import java.util.*;
import java.util.stream.Stream;

public class Main {

    public static void main(String[] args){
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        Stream<Integer> stream = list.stream()
                .filter(e -> e > 3);

        long count = stream.count();
        System.out.print(count); // 2
    }
}

list の中身を絞ったので Stream の要素数は 4,5 の2つになっています。 よって、count は2で想定通りの挙動をしています。

ちなみ、1度 stream に格納しなくても一発で終端操作まで行うこともできます。

import java.util.*;

public class Main {

    public static void main(String[] args){
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        long count = list.stream()
                .filter(e -> e > 3)
                .count();
        
        System.out.print(count); // 2
    }
}

allMatch メソッド

これは stream の全ての要素が指定した条件に合致すれば true を返します。
さきほどの map メソッドを持った stream に対して allMatch を適用させてみましょう。

import java.util.*;

public class Main {

    public static void main(String[] args) {
        List<Book> bookList = Arrays.asList(
                new Book(1, "Java 入門"),
                new Book(2, "Java 応用"),
                new Book(3, "Python 入門"));

        boolean isContainJava = bookList.stream()
                .map(e -> e.getName())
                .allMatch(e -> e.contains("Java"));

        System.out.println(isContainJava); // false
    }
}

class Book {
    int id;
    String name;

    Book(int id, String name) {
        this.id = id;
        this.name = name;
    }

    String getName() {
        return this.name;
    }
}

すべての要素が Java という文字列を含んでいるかチェックしましたが、stream 最後の要素は “Python 入門” であり Java を含んでいないので false になります。
では、Python 入門 を filter で除去したらどうなるでしょうか??

import java.util.*;

public class Main {

    public static void main(String[] args){
        List<Book> bookList = Arrays.asList(
                new Book(1, "Java 入門"),
                new Book(2, "Java 応用"),
                new Book(3, "Python 入門"));

        boolean isContainJava = bookList.stream()
                .map(e -> e.getName())
                .filter(e -> !e.contains("Python"))
                .allMatch(e -> e.contains("Java"));

        System.out.println(isContainJava);
    }
}

class Book{
    int id;
    String name;

    Book(int id, String name){
        this.id = id;
        this.name = name;
    }

    String getName(){
        return this.name;
    }
}

filter で Python という文字列を含んだ要素を除去しているため、allMatch は true になります。

toList メソッド

これが終端操作で1番使用する終端操作かもしれません。 処理は単純で Strem をリストに変換するだけです。

import java.util.*;

public class Main {

    public static void main(String[] args){
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        List<Integer> numList = list.stream()
                .filter(e -> e > 3)
                .toList();

        for(int num : numList){
            System.out.println(num); // 4, 5 を表示
        }
    }
}

これで list の中身を加工した新しいリスト numList を作成することができました。

終端操作を行うことで Steram から新しいリストを作成したり、要素数を数えるなどの操作が可能になります。

おまけ

最後におまけです。

ブログ内で「終端処理が実行されていない段階では中間操作は実行されていない」と述べましたが、ピンと来ていない方も多いと思うので実行箇所を可視化してみます。

import java.util.*;
import java.util.stream.Stream;

public class Main {

    public static void main(String[] args){
        List<Book> bookList = Arrays.asList(
                new Book(1, "Java 入門"),
                new Book(2, "Java 応用"),
                new Book(3, "Python 入門"));

        Stream<String> bookStream = bookList.stream()
                .map(e ->{
                        System.out.println(e.getName());
                        return e.getName();
                })
                .filter(e -> !e.contains("Python"));

        System.out.println("最初に表示される?");

        boolean isContainJava = bookStream.allMatch(e -> e.contains("Java"));

        System.out.println(isContainJava);
    }
}

class Book{
    int id;
    String name;

    Book(int id, String name){
        this.id = id;
        this.name = name;
    }

    String getName(){
        return this.name;
    }
}

Stream 作成時点では map が実行されないので、「最初に表示される?」が始めに表示され、その後に各 Book の name と 全要素が Java という文字列を含んでいるかを boolean で表示します。

実行結果

最初に表示される?
Java 入門
Java 応用
Python 入門
true

こんな感じで終端操作が実行されるタイミングで中間操作も実行されるので覚えておきましょう!

さいごに

いかがだったでしょうか?

今回で Stream 理解への道は最後になります。 Java 初学者の方は Stream でつまづきやすいので私のブログがみなさんのお役に立てれば光栄です。 これからも Java. ライフを楽しんでいきましょう!

投稿者 Sawa