🐡

ストリームAPIのまとめ(後編)[Java入門]

に公開

はじめに

こんにちは。
プログラミング初心者wakinozaと申します。
Java勉強中に調べたことを記事にまとめています。

十分気をつけて執筆していますが、なにぶん初心者が書いた記事なので、理解が浅い点などあるかと思います。
間違い等あれば、指摘いただけると助かります。

記事を参考にされる方は、初心者の記事であることを念頭において、お読みいただけると幸いです。

対象読者

  • Javaを勉強中の方
  • Java SE11 Gold試験を勉強中の方
  • JavaのストリームAPIのメソッドを知りたい方

目次

1. インスタンス生成のメソッド
2. 中間操作のメソッド
3. 終端操作のメソッド

本文

この記事では、ストリームAPIの主要メソッドについてまとめています。
ストリームAPIの概要は、前回の記事をご覧ください。

https://zenn.dev/wakinoza/articles/b5cb3cce99d4c2

1. インスタンス生成のメソッド

この章では、主にListやSet、配列、Mapからストリームインスタンスを生成するメソッドを紹介していきます。

1. ListやSetからストリームインスタンスを生成

リストやセットからストリームインスタンスを生成する場合は、コレクションインターフェースのstream()メソッドを利用します。

default Stream<E> stream()

List<String> fruits = Arrays.asList("apple", "orange", "banana");
fruits.stream()
      .forEach(s -> System.out.println(s));

2. 配列からストリームインスタンスを生成

配列からストリームインスタンスを生成する場合は、Arraysクラスのstream()メソッドを利用します。オーバーロードされており、引数によって戻り値となるstreamの種類が異なります。

public static <T> Stream<T> stream(T[] array)
・参照型の配列の時はstreamのインスタンスを返します

public static IntStream stream(int[] array)
・int型の配列の時はIntStreamのインスタンスを返します

public static LongStream stream(long[] array)
・long型の配列の時はLongStreamのインスタンスを返します

public static DoubleStream stream(double[] array)
・double型の配列の時はDoubleStreamのインスタンスを返します

String[] fruits = {"apple", "orange", "banana"};
Arrays.stream(fruits)
    .forEach(s -> System.out.println(s));

もし、ストリームインスタンスを生成したい値が決まっている場合は、配列を作成せずに、Streamクラスのファクトリメソッドで直接ストリームインスタンスを生成する事ができます。この場合は、Streamクラスのof()メソッドを利用します。

static <T> Stream<T> of(T t)

Stream<String> stream = Stream.of("apple", "orange", "banana");
stream.forEach(s -> System.out.println(s));

3. Mapからストリームインスタンスを生成

マップのキーや値からストリームインスタンスを生成したい場合は、KsySet()メソッドやValues()メソッドでキーや値のセットを取り出します。keySet()はSetを、values()はCollectionを返します。

Map<String, String> fruits = new HashMap();
fruits.put("1", "apple");
fruits.put("2", "orange");
fruits.put("3", "banana");

//キーからストリームインスタンスを生成する場合
Stream<String> stream1 = fruits.keySet().stream();
stream1.forEach(s -> System.out.println(s));

//値からストリームインスタンスを生成する場合
Stream<String> stream2 = fruits.values().stream();
stream2.forEach(s -> System.out.println(s));

キーと値の両方を使ってストリームインスタンスを生成する場合は、entrySet()メソッドによりSetを取得して、ストリームインスタンスを生成します。

Map<String, String> fruits = new HashMap();
fruits.put("1", "apple");
fruits.put("2", "orange");
fruits.put("3", "banana");

//キーと値からストリームインスタンスを生成する場合
Stream<Entry<String, String>> stream = fruits.entrySet().stream();
stream.forEach(s -> System.out.println(s.getKey() + ":" + s.getValue()));

2. 中間操作のメソッド

この章では、代表的な中間操作メソッドを機能別にまとめていきます。

1. 要素を置き換える中間操作

「map」という名前がついたメソッドは、主に要素の置き換えるためのメソッドです。

1. map

<R> Stream<R> map(Function<? super T,? extends R> mapper)

  • 引数にFunction型のラムダ式を受け取り、ストリーム内の要素を処理し、新しいストリームインスタンスを返します
  • このメソッドのポイントは、元々の要素の型と、処理後の要素の型を一致させる必要がないという事です。元々の要素の型と処理後の要素の型が同じでも良いですし、処理の型を任意の型に変換することもできます。処理だけではなく、型の変換までできる点が、mapメソッドの特徴です
2. mapToInt

IntStream mapToInt(ToIntFunction<? super T> mapper)

  • 引数にFunction型のラムダ式を受け取り、ストリーム内の要素を処理し、IntStreamインスタンスを返します。処理後の要素の型は、int型になります
3. mapToLong

LongStream mapToLong(ToLongFunction<? super T> mapper)

  • 引数にFunction型のラムダ式を受け取り、ストリーム内の要素を処理し、LongStreamインスタンスを返します。処理後の要素の型は、long型になります
4. mapToDouble

DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)

  • 引数にFunction型のラムダ式を受け取り、ストリーム内の要素を処理し、DoubleStreamインスタンスを返します。処理後の要素の型は、double型になります
4. flatMap

<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

  • flatMapは、引数にFunction型のラムダ式を受け取り、ストリーム内の要素を処理するところまではMapと同じ働きをします。Mapとの違いは、処理後に新しいストリームインスタンスを返す工程にあります。mapは各要素をそのまま変換するのに対し、flatMapは各要素を持つ新しいストリームに変換し、それらのストリームをすべて連結して一つの平坦なストリームを生成します。結合した際は、元の要素数と処理後の要素数は異なる場合がある点もflatMapの特徴です
  • flatMapが使われるのは、入れ子状態になった要素を平坦化したい場合です。もし、「コレクションのコレクション」といった、入れ子状態の要素を扱う場合、Mapを利用すると、処理前と同様に入れ子状態のまま結果が返ります。しかし、flatMapを利用すると、入れ子になったコレクションを平坦化してくれるので、単一のコレクションに要素を変換できます
List<List<String>> listOfLists = Arrays.asList(
    Arrays.asList("Alice", "Bob"),
    Arrays.asList("Charlie", "David", "Eve")
);

List<List<String>> resultWithMap = listOfLists.stream()
                                              .map(list -> list) 
                                              // 各リスト自体をそのまま返す(あるいは何らかの変換を適用しても良い)
                                              .collect(Collectors.toList());

System.out.println("mapを使った結果: " + resultWithMap);
// 出力: mapを使った結果: [[Alice, Bob], [Charlie, David, Eve]]

// flatMapを使った場合、flatMap処理後はStream<String>となり、最終結果はList<String>となる
List<String> resultWithFlatMap = listOfLists.stream()
                                            .flatMap(list -> list.stream()) 
                                            // 各内部リストをストリームに変換し、平坦化
                                            .collect(Collectors.toList());

System.out.println("flatMapを使った結果: " + resultWithFlatMap);
// 出力: flatMapを使った結果: [Alice, Bob, Charlie, David, Eve]

上のコードでは、入れ子状態のリストlistOfListsを処理しています。
mapを使った場合、map処理後は Stream<List<String>> となり、最終結果はList<List<String>> となります。
一方、flatMapを使った場合、flatMap処理後はStream<String>となり、最終結果はList<String>となります。
もし、入れ子状態のコレクションや配列の情報を平坦化して扱いたい場合は、flatMapを利用すると良いでしょう。

また、今回は個別には紹介しませんが、プリミティブ型のストリームを返すflatMapも存在します。

IntStream flatMapToInt(Function<? super T,? extends IntStream> mapper)
LongStream flatMapToLong(Function<? super T,? extends LongStream> mapper)
DoubleStream flatMapToDouble(Function<? super T,? extends DoubleStream> mapper)

2. 要素を絞り込む中間操作

filter()、limit()、distinct()などは要素を絞り込む中間操作です。絞り込むため、要素の数は減りますが、要素の型は変更されません。

1. filter

Stream<T> filter(Predicate<? super T> predicate)

  • 引数にPredicate型のラムダ式を受け取り、条件に合致した要素のみに絞り込みます
2. limit

Stream<T> limit(long maxSize)

  • filter()メソッドは、ストリームの要素数を、引数で指定された数に制限します
  • filter()は、ショートサーキット操作であるため、引数で指定された数の要素が見つかった時点で、それ以上上流のストリームを処理するのを停止します。これによって無限ストリームを有限ストリームに変換したり、多量のデータソースのうち少量のデータしか利用しない場合などでパフォーマンスを向上させる事ができます
  • 注意が必要な点は、処理を行う順番です。limit()メソッドは、元のコレクションが保持している順番に応じて処理を実行します。元のコレクションがArrayListのような要素を追加した順番を管理している場合は要素順で処理が行われますが、HashSetのようなハッシュコードの順番で管理している場合は、順序が保証されないため、意図した順序で処理されない可能性があります
  • 引数が負の場合は、IllegalArgumentExceptionをスローします
3. distinct

Stream<T> distinct()

  • 要素内の重複した要素を取り除きます
  • 要素が重複しているかどうかは、equals()メソッドがtrueかどうかで判断されます

3. 要素を並べ替える中間操作

  • sorted()メソッドは、要素を指定された順番に並べ替えます
  • 2つのオーバーロードがあります

Stream<T> sorted()

  • 引数を指定しない場合は、要素が保持する自然順序に従って並べ替えます

Stream<T> sorted(Comparator<? super T> comparator)

  • 引数にComparatorインターフェースのラムダ式を渡すと、指定された順序に並べ替えます

4. デバック処理で使用する中間操作

Stream<T> peek(Consumer<? super T> action)

  • 主にデバッグなどの目的で、ストリームパイプラインの途中の状態を確認するのに用います
  • 引数としてConsumer型のラムダ式を受け取ります

3. 終端操作のメソッド

ストリームは一度消費されると、再利用できないため、終端操作は1回しか呼び出せません。もし、2回以上呼び出すと、IllegalStateExceptionをスローします。

1. 繰り返し処理を行う終端操作

1. forEach

void forEach(Consumer<? super T> action)

  • Consumer型のラムダ式を受け取り、ストリームの各要素に処理を行います
  • 処理順は、limit()の時と同様に、元のコレクションが保持する順番に応じて処理が実行されます。HashSetなどハッシュコードの順番で処理が行われるため、意図した順序で処理されない可能性があります
  • また、並列ストリームでforEach()メソッドを利用すると、複数スレッドで一斉に処理が行われるため、処理の順番をプログラムで制御する事ができません。その結果、順番がランダムに前後する可能性があります
2. forEachOrdered

void forEachOrdered(Consumer<? super T> action)

  • 並列ストリームで順番を守りながら繰り返し処理をしたい場合は、forEachOrdered()を利用します
  • forEachOrdered()メソッドはforEach()メソッドと同様に、各要素に引数の処理を行います。その上、並列ストリームの環境下でも、コレクションが保持する順番を守って処理が実行できます

2. 結果を一つだけ取り出す終端操作

ストリームの要素から、1つの要素だけを取り出す操作です。

1. findAny

Optional<T> findAny()

  • ストリームの「最初に処理した要素」を格納するOptionalを返します。ストリームに一致する要素がない場合は空のOptionalを返します
  • 「最初の要素」ではなく、「最初に処理した要素」であることがfindAny()メソッドの特徴です
  • その特徴は、並列ストリームで処理を行った場合に顕著に現れます。並列処理されている複数の要素のうちどれが「最初の処理した要素」となるかはランダムで決まります。そのため、並列ストリームでfindAny()を利用すると、実行する度に値が変わる可能性があります
  • 並列ストリームの際に、値がランダムになる可能性はありますが、一方で並列処理でのパフォーマンスを最大化できます
  • 要素にnullが含まれる場合は、NullPointerExceptionをスローします
// 並列ストリームでfindAny()を使用
List<String> moreNames = Arrays.asList("Alice", "Amy", "Bob", "Anna", "Charlie");
Optional<String> anyElementParallel = moreNames.parallelStream()
                                               .filter(s -> s.startsWith("A"))
                                               .findAny();
anyElementParallel.ifPresent(s -> System.out.println(s));
// 実行ごとに異なる結果になる可能性がある
// 結果: 「Alice」または「Amy」または「Anna」など
2. findFirst

Optional<T> findFirst()

  • ストリームの最初の要素を格納するOptionalを返します。ストリームに一致する要素がない場合は空のOptionalを返します
  • findAny()メソッドとの違いは、並列ストリーム時にも要素の順番を保持する点です
  • 順番を保持するため、並列処理のパフォーマンスはfindAny()メソッドより劣ります
  • 要素にnullが含まれる場合は、NullPointerExceptionをスローします
List<String> names = Arrays.asList("Alice", "Amy", "Bob", "Anna", "Charlie", "David");

// 並列ストリームでfindFirst()を使用
Optional<String> firstElementParallel = names.parallelStream()
                                              .filter(s -> s.startsWith("A"))
                                              .findFirst();

firstElementParallel.ifPresent(s -> System.out.println(s)); 
// 結果: Alice
3. min

Optional<T> min(Comparator<? super T> comparator)

  • 引数にComparatorのラムダ式を受け取ります。ラムダ式の順序に従ってストリーム内の最小の要素を探し、最小値を格納したOptionalを返します。該当する要素がない場合は、空のOptionalを返します
  • 要素にnullが含まれる場合は、NullPointerExceptionをスローします
4. max

Optional<T> max(Comparator<? super T> comparator)

  • 引数にComparatorのラムダ式を受け取ります。ラムダ式の順序に従ってストリーム内の最大の要素を探し、最大値を格納したOptionalを返します。該当する要素がない場合は、空のOptionalを返します
  • 要素にnullが含まれる場合は、NullPointerExceptionをスローします

3. 集計処理を行う終端操作

1. count

long count()

  • ストリームの要素の個数をlong型で返します
2. min

OptionalInt/OptionalDouble/OptionalLong min()

  • IntStream/DoubleStream/LongStreamなどのプリミティブ型のストリームでのみ利用できます
  • 最小の要素を、OptionalInt/OptionalDouble/OptionalLong型のいずれかで返します。ストリームが空の場合は、空のOptionalInt/OptionalDouble/OptionalLong型のいずれかを返します
  • 参照型のStreamで利用されるmin()と違い、引数は不要です
3. max

OptionalInt/OptionalDouble/OptionalLong max()

  • IntStream/DoubleStream/LongStreamなどのプリミティブ型のストリームでのみ利用できます
  • 最大の要素を、OptionalInt/OptionalDouble/OptionalLong型のいずれに格納して返します。ストリームが空の場合は、空のOptionalInt/OptionalDouble/OptionalLong型のいずれかを返します
  • 参照型のStreamで利用されるmax()と違い、引数は不要です
4. sum

int/long/double sum()

  • IntStream/DoubleStream/LongStreamなどのプリミティブ型のストリームでのみ利用できます
  • ストリームの要素の合計値をint/long/doubleで返します
5. average

OptionalDouble average()

  • IntStream/DoubleStream/LongStreamなどのプリミティブ型のストリームでのみ利用できます
  • 要素の平均をOptionalDoubleのインスタンスに格納して返します。ストリームが空の場合は、空のOptionalDoubleを返します
  • average()メソッドの戻り値がOptionalDoubleに限定されているのは、要素の割り算が行われると結果が整数になるとは限らないためです

4. 結果をまとめて取り出す終端操作

結果をまとめて取り出す終端操作です。

1. toArray

Object[] toArray()

  • ストリームの要素を配列にして返します
2. reduce
  • ストリームの要素を単一の値に集約します。sum(), count(), min(), max(), average()などの専用の終端操作では対応できない集約処理を行いたい場合に用います
  • 3つのオーバーロードが存在します

Optional<T> reduce(BinaryOperator<T> accumulator)

  • 引数が1つの際は、accumulator関数のラムダ式を受け取ります。ストリームの最初の要素を初期値としてラムダ式を実行し、最終的な結果をOptionalインスタンスで返します。ストリームに要素がない場合は、空のOptionalを返します
  • 要素にnullがある場合は、NullPointerExceptionをスローします
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream()
                                .reduce((result, element) -> result + element);
sum.ifPresent(s -> System.out.println(s));
 // 結果:: 15

T reduce(T identity, BinaryOperator<T> accumulator)

  • 引数が2つの際は、初期値とaccumulator関数のラムダ式を受け取ります
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Integer sum = numbers.stream()
                      .reduce(10, (result, element) -> result + element);
System.out.println(sum);
// 結果: 25

<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

  • 引数が3つの際は、初期値とaccumulator関数のラムダ式とcombiner関数のラムダ式を受け取ります
  • 並列処理を効率的に行うため、並列処理中に生成された複数の部分的な結果を結合するcombiner関数を実行する
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Integer sum = numbers.parallelStream()
                     .reduce(0,
                      (result, element) -> result + element,
                      (sum1, sum2) -> sum1 + sum2 
                     );
System.out.println(sum); 
// 結果 : 15
3. collect
  • collect()メソッドは、ストリーム内の要素を結果コンテナに蓄積し、引数で指示された処理を行い、最終的な表現に変換します
  • collect()メソッドには2つのオーバーロードがあります

<R, A> R collect(Collector<? super T,A,R> collector)

  • 引数1つのメソッドは、引数として受け取るCollectorインターフェースの内容によって、ストリーム内の要素をコレクションに変換したり、要素をグループ化したり、合計や平均などを計算したりといった、様々な処理を実現します
  • Collectorインターフェースは、開発者が独自に実装することもできますが、一般的な操作の多くは、Collectorsクラスとして提供されています
  • Collectorsクラスは、Collectorインターフェースの便利な実装を提供するユーティリティクラスです。Collectorsクラスを利用することで、Collectorインターフェースを実装することなく、簡単にcollectメソッドを利用することができます
  • collect()メソッドを用いる場合は、ほどんどのケースではこの引数が1つのメソッドを利用します

<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)

  • 引数の3つのCollect()メソッドは、Collectorsクラスでは実現できない処理を行いたいが、Collectorインターフェースを独自実装するほど複雑な処理ではないという場合に、シンプルな処理方法を引数内で定義したい場合に用います

次に、代表的なCollectorsクラスのメソッドを紹介します。

1. toList

public static <T> Collector<T,?,List<T>> toList()

  • ストリーム内の要素をListに変換して返します
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

List<String> upperCaseNames = names.stream()
                                .map(String::toUpperCase)
                                .collect(Collectors.toList());
System.out.println(upperCaseNames);
// 結果:[ALICE, BOB, CHARLIE, DAVID]
2. toSet

public static <T> Collector<T,?,Set<T>> toSet()

  • ストリーム内の要素をSetに変換して、返します。重複している要素は、自動的に省かれます
List<Integer> numbers = Arrays.asList(1, 2, 3, 2, 4, 1);

Set<Integer> uniqueNumbers = numbers.stream()
                                    .collect(Collectors.toSet());
System.out.println(uniqueNumbers); 
// 結果:[1, 2, 3, 4] (順序は保証されない)
3. toMap
  • ストリーム内の要素をMapに変換して、返します
  • 3つのオーバーロードがあります

public static <T, K, U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper)

  • 引数が2つのときは、Function型の関数を2つ受け取ります。1つ目のFunction型はストリームの要素からMapのキーを抽出する関数、2つ目のFunction型はストリームの要素からMapの値を抽出する関数です
  • Mapしたキーに重複した要素があれば、IllegalStateExceptionがスローされます

public static <T, K, U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction)

  • 引数が3つの時は、Function型の関数を2つとBinaryOperator型の関数を1つ受け取ります。1つ目Function型はストリームの要素からMapのキーを抽出する関数、2つ目Function型はストリームの要素からMapの値を抽出する関数、BinaryOperator型はキーが重複した場合に、そのキーに対応する値をどのように結合するかを定義する関数です

public static <T, K, U, M extends Map<K, U>> Collector<T,?,M> toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapFactory)

  • 引数が4つの時は、Function型の関数を2つ、BinaryOperator型の関数を1つ、Supplier型の関数を1つ受け取ります。Function型の関数2つとBinaryOperator型の関数1つの役割は、他のオーバーロードと同様です。Supplier型の関数は、生成される Map の具体的な実装(HashMap, TreeMap, LinkedHashMap など)を指定します
4. joining
  • ストリームの要素を1つの文字列に結合します
  • 3つのオーバーロードがあります

public static Collector<CharSequence,?,String> joining()

  • 引数なしの場合は、要素をそのまま順に結合していきます
List<String> words = Arrays.asList("Hello", "Java", "Stream", "API");
String result = words.stream()
                      .collect(Collectors.joining());

System.out.println(result);
//結果: HelloJavaStreamAPI

public static Collector<CharSequence,?,String> joining(CharSequence delimiter)

  • 引数1つの場合は、ストリーム内の要素を、引数の区切り文字(デリミタ)で区切りながら結合します

public static Collector<CharSequence,?,String> joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)

  • 引数3つの場合は、ストリーム内の要素を、引数の区切り文字(デリミタ)で区切りながら結合した後、2つ目の引数で指定された接頭辞と、3つ目の引数で指定された接尾辞を付加して、返します
5. groupingBy
  • ストリーム内の要素をグループ化したMapを作成します
  • 3つのオーバーロードがあります

public static <T, K> Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier)

  • 引数が1つの場合は、Function型の関数を受け取ります。Function型はストリームの要素を分類する関数です。グループ化した要素は、グループをキー、そのキーを持つすべてのストリーム要素のListを値とするMapを返します

class Person {
    String name;
    String city;
    int age;

    public Person(String name, String city, int age) {
        this.name = name;
        this.city = city;
        this.age = age;
    }

    public String getName() { return name; }
    public String getCity() { return city; }
    public int getAge() { return age; }
}

public class GroupingByOverloads {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
            new Person("Alice", "New York", 30),
            new Person("Bob", "London", 25),
            new Person("Charlie", "New York", 35),
            new Person("David", "London", 28),
            new Person("Eve", "Paris", 22)
        );

        //都市ごとにグループ化する 
        Map<String, List<Person>> peopleByCity = people.stream()
                .collect(Collectors.groupingBy(Person::getCity));

        peopleByCity.forEach((city, personList) -> 
            System.out.println("  " + city + ": " + personList));
        //結果 : 
        //London: [Bob(London, 25), David(London, 28)]
        //Paris: [Eve(Paris, 22)]
        //New York: [Alice(New York, 30), Charlie(New York, 35)]
    }
}

public static <T, K, A, D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)

  • 引数が2つの場合は、Function型の関数1つとCollector型の関数1つを受け取ります。Function型はストリームの要素を分類する関数で、Collector型の関数はグループ化した要素への追加の処理内容を表します

public static <T, K, D, A, M extends Map<K, D>> Collector<T,?,M> groupingBy(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream)

  • 引数が3つの場合は、Function型の関数とSuplier型の関数とCollector型の関数を受け取ります。Function型とCollector型の関数は他のオーバーロードを同様です。Supplier型の関数が生成される Map の具体的な実装(HashMap, TreeMap, LinkedHashMap など)を供給する関数です

記事は以上です。
最後までお読みいただき、ありがとうございました。

参考情報一覧

この記事は以下の情報を参考にして執筆しました。

GitHubで編集を提案

Discussion