Java 8 Stream APIチュートリアル

Java 8 Stream APIチュートリアル

1. 概要

この詳細なチュートリアルでは、Java8ストリームの作成から並列実行までの実際の使用法について説明します。

この資料を理解するには、読者はJava 8(ラムダ式、Optional,メソッド参照)およびStreamAPIの基本的な知識を持っている必要があります。 これらのトピックに精通していない場合は、以前の記事–New Features in Java 8およびIntroduction to Java 8 Streamsをご覧ください。

参考文献:

ラムダ式と機能的インターフェース:ヒントとベストプラクティス

Java 8ラムダと機能インターフェースの使用に関するヒントとベストプラクティス。

Java 8のコレクターのガイド

この記事では、Java 8コレクターについて説明し、組み込みコレクターの例を示し、カスタムコレクターの構築方法を示します。

2. ストリームの作成

さまざまなソースのストリームインスタンスを作成する方法は多数あります。 したがって、インスタンスwill not modify its source,が作成されると、単一のソースから複数のインスタンスを作成できます。

2.1. 空のストリーム

空のストリームを作成する場合は、empty()メソッドを使用する必要があります。

Stream streamEmpty = Stream.empty();

要素のないストリームに対してnullが返されないようにするために、作成時にempty()メソッドが使用されることがよくあります。

public Stream streamOf(List list) {
    return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}

2.2. Collectionのストリーム

ストリームは、任意のタイプのCollectionCollection, List, Set)で作成することもできます。

Collection collection = Arrays.asList("a", "b", "c");
Stream streamOfCollection = collection.stream();

2.3. 配列のストリーム

配列は、ストリームのソースにもなります。

Stream streamOfArray = Stream.of("a", "b", "c");

既存の配列または配列の一部から作成することもできます。

String[] arr = new String[]{"a", "b", "c"};
Stream streamOfArrayFull = Arrays.stream(arr);
Stream streamOfArrayPart = Arrays.stream(arr, 1, 3);

2.4. Stream.builder()

When builder is usedthe desired type should be additionally specified in the right part of the statement,それ以外の場合、build()メソッドはStream<Object>:のインスタンスを作成します

Stream streamBuilder =
  Stream.builder().add("a").add("b").add("c").build();

2.5. Stream.generate()

generate()メソッドは、要素生成にSupplier<T>を受け入れます。 結果のストリームは無限であるため、開発者は目的のサイズを指定する必要があります。そうしないと、メモリ制限に達するまでgenerate()メソッドが機能します。

Stream streamGenerated =
  Stream.generate(() -> "element").limit(10);

上記のコードは、値–“element”の10個の文字列のシーケンスを作成します。

2.6. Stream.iterate()

無限ストリームを作成する別の方法は、iterate()メソッドを使用することです。

Stream streamIterated = Stream.iterate(40, n -> n + 2).limit(20);

結果のストリームの最初の要素は、iterate()メソッドの最初のパラメーターです。 次のすべての要素を作成するために、指定された関数が前の要素に適用されます。 上記の例では、2番目の要素は42になります。

2.7. プリミティブのストリーム

Java 8は、次の3つのプリミティブ型からストリームを作成する可能性を提供します。int, longdouble.Stream<T>はジェネリックインターフェイスであり、ジェネリックの型パラメーターとしてプリミティブを使用する方法はありません。 3つの新しい特別なインターフェースが作成されました:IntStream, LongStream, DoubleStream.

新しいインターフェイスを使用すると、不要な自動ボックス化が軽減され、生産性が向上します。

IntStream intStream = IntStream.range(1, 3);
LongStream longStream = LongStream.rangeClosed(1, 3);

range(int startInclusive, int endExclusive)メソッドは、最初のパラメーターから2番目のパラメーターへの順序付きストリームを作成します。 1に等しいステップで後続の要素の値をインクリメントします。 結果には最後のパラメータは含まれていません。これはシーケンスの上限にすぎません。

rangeClosed(int startInclusive, int endInclusive)メソッドは同じことを行いますが、違いは1つだけです。つまり、2番目の要素が含まれています。 これらの2つの方法を使用して、3種類のプリミティブストリームのいずれかを生成できます。

Java 8以降、Randomクラスは、プリミティブのストリームを生成するためのさまざまなメソッドを提供します。 たとえば、次のコードは、3つの要素を持つDoubleStream,を作成します。

Random random = new Random();
DoubleStream doubleStream = random.doubles(3);

2.8. Stringのストリーム

Stringは、ストリームを作成するためのソースとしても使用できます。

Stringクラスのchars()メソッドの助けを借りて。 JDKにはインターフェースCharStreamがないため、代わりにIntStreamを使用して文字のストリームを表します。

IntStream streamOfChars = "abc".chars();

次の例では、Stringを指定されたRegExに従ってサブ文字列に分割します。

Stream streamOfString =
  Pattern.compile(", ").splitAsStream("a, b, c");

2.9. ファイルのストリーム

Java NIOクラスFilesを使用すると、lines()メソッドを使用してテキストファイルのStream<String>を生成できます。 テキストのすべての行がストリームの要素になります。

Path path = Paths.get("C:\\file.txt");
Stream streamOfStrings = Files.lines(path);
Stream streamWithCharset =
  Files.lines(path, Charset.forName("UTF-8"));

Charsetは、lines()メソッドの引数として指定できます。

3. a Streamの参照

中間操作のみが呼び出されている限り、ストリームをインスタンス化し、アクセス可能な参照を持つことができます。 ターミナル操作を実行すると、ストリームにアクセスできなくなります.

これを実証するために、ベストプラクティスは一連の操作を連鎖させることであることをしばらく忘れます。 不要な冗長性に加えて、技術的には次のコードが有効です。

Stream stream =
  Stream.of("a", "b", "c").filter(element -> element.contains("b"));
Optional anyElement = stream.findAny();

ただし、ターミナル操作を呼び出した後に同じ参照を再利用しようとすると、IllegalStateException:がトリガーされます

Optional firstElement = stream.findFirst();

IllegalStateExceptionRuntimeExceptionであるため、コンパイラーは問題について通知しません。 したがって、Java 8streams can’t be reused.であることを覚えておくことが非常に重要です。

ストリームは、機能スタイルの要素のソースに有限の操作シーケンスを適用する機能を提供するように設計されているが、要素を格納するためではないため、この種の動作は論理的です。

したがって、以前のコードを適切に機能させるには、いくつかの変更を行う必要があります。

List elements =
  Stream.of("a", "b", "c").filter(element -> element.contains("b"))
    .collect(Collectors.toList());
Optional anyElement = elements.stream().findAny();
Optional firstElement = elements.stream().findFirst();

4. ストリームパイプライン

データソースの要素に対して一連の操作を実行し、その結果を集計するには、sourceintermediate operation(s)、およびterminal operation.の3つの部分が必要です。

中間操作は、変更された新しいストリームを返します。 たとえば、要素が少ない既存のストリームの新しいストリームを作成するには、skip()メソッドを使用する必要があります。

Stream onceModifiedStream =
  Stream.of("abcd", "bbcd", "cbcd").skip(1);

複数の変更が必要な場合は、中間操作を連鎖できます。 また、現在のStream<String>のすべての要素を、最初の数文字の部分文字列に置き換える必要があると想定します。 これは、skip()メソッドとmap()メソッドをチェーンすることによって行われます。

Stream twiceModifiedStream =
  stream.skip(1).map(element -> element.substring(0, 3));

ご覧のとおり、map()メソッドはラムダ式をパラメーターとして受け取ります。 ラムダについて詳しく知りたい場合は、チュートリアルLambda Expressions and Functional Interfaces: Tips and Best Practicesをご覧ください。

ストリーム自体には価値がありません。ユーザーが関心を持っているのは、端末操作の結果です。これは、何らかのタイプの値またはストリームのすべての要素に適用されるアクションです。 Only one terminal operation can be used per stream.

ストリームを使用するための適切で最も便利な方法は、stream pipeline, which is a chain of stream source, intermediate operations, and a terminal operation.によるものです。次に例を示します。

List list = Arrays.asList("abc1", "abc2", "abc3");
long size = list.stream().skip(1)
  .map(element -> element.substring(0, 3)).sorted().count();

5. 怠惰な呼び出し

Intermediate operations are lazy.これは、they will be invoked only if it is necessary for the terminal operation execution.を意味します

これを示すために、呼び出されるたびに内部カウンターをインクリメントするメソッドwasCalled(),があると想像してください。

private long counter;

private void wasCalled() {
    counter++;
}

操作filter()からメソッドwasCalled()を呼び出しましょう:

List list = Arrays.asList(“abc1”, “abc2”, “abc3”);
counter = 0;
Stream stream = list.stream().filter(element -> {
    wasCalled();
    return element.contains("2");
});

3つの要素のソースがあるため、メソッドfilter()が3回呼び出され、counter変数の値が3になると想定できます。 ただし、このコードを実行してもcounterはまったく変更されず、ゼロのままであるため、filter()メソッドは一度も呼び出されませんでした。 理由は、ターミナル操作の欠落です。

map()操作とターミナル操作–findFirst().を追加して、このコードを少し書き直してみましょう。また、ロギングを使用してメソッド呼び出しの順序を追跡する機能も追加します。

Optional stream = list.stream().filter(element -> {
    log.info("filter() was called");
    return element.contains("2");
}).map(element -> {
    log.info("map() was called");
    return element.toUpperCase();
}).findFirst();

結果のログは、filter()メソッドが2回呼び出され、map()メソッドが1回だけ呼び出されたことを示しています。 これは、パイプラインが垂直に実行されるためです。 この例では、ストリームの最初の要素がフィルターの述語を満たさなかったため、フィルターを通過した2番目の要素に対してfilter()メソッドが呼び出されました。 3番目の要素のfilter()を呼び出さずに、パイプラインを経由してmap()メソッドに移動しました。

findFirst()演算は、1つの要素だけで満たされます。 したがって、この特定の例では、遅延呼び出しにより、2つのメソッド呼び出し(1つはfilter()用、もう1つはmap().用)を回避できました。

6. 実行の順序

パフォーマンスの観点から、the right order is one of the most important aspects of chaining operations in the stream pipeline:

long size = list.stream().map(element -> {
    wasCalled();
    return element.substring(0, 3);
}).skip(2).count();

このコードを実行すると、カウンターの値が3増加します。 これは、ストリームのmap()メソッドが3回呼び出されたことを意味します。 ただし、sizeの値は1です。 したがって、結果のストリームには要素が1つだけあり、3回のうち2回、理由もなく高価なmap()操作を実行しました。

skip()メソッドとmap()メソッド,の順序を変更すると、counterは1つだけ増加します。 したがって、メソッドmap()は1回だけ呼び出されます。

long size = list.stream().skip(2).map(element -> {
    wasCalled();
    return element.substring(0, 3);
}).count();

これにより、次のルールが適用されます。intermediate operations which reduce the size of the stream should be placed before operations which are applying to each element.したがって、skip(), filter(), distinct()などのメソッドをストリームパイプラインの先頭に保持します。

7. ストリーム削減

APIには、ストリームをタイプまたはプリミティブ(count(), max(), min(), sum(),など)に集約する多くの端末操作がありますが、これらの操作は事前定義された実装に従って機能します。 そして、何if a developer needs to customize a Stream’s reduction mechanism?これを可能にする2つのメソッドがあります–reduce()メソッドとcollect()メソッドです。

7.1. reduce()メソッド

このメソッドには3つのバリエーションがあり、シグネチャと戻り値の型が異なります。 次のパラメーターを持つことができます。

identity –アキュムレータの初期値、またはストリームが空で累積するものがない場合のデフォルト値。

accumulator –は、要素の集約のロジックを指定する関数です。 アキュムレータは削減のすべてのステップで新しい値を作成するため、新しい値の量はストリームのサイズに等しく、最後の値のみが役立ちます。 これはパフォーマンスにはあまり適していません。

combiner –は、アキュムレータの結果を集計する関数です。 Combinerは、異なるスレッドからのアキュムレーターの結果を減らすために、並列モードでのみ呼び出されます。

それでは、次の3つの方法を実際に見てみましょう。

OptionalInt reduced =
  IntStream.range(1, 4).reduce((a, b) -> a + b);

reduced = 6(1 + 2 + 3)

int reducedTwoParams =
  IntStream.range(1, 4).reduce(10, (a, b) -> a + b);

reducedTwoParams = 16(10 + 1 + 2 + 3)

int reducedParams = Stream.of(1, 2, 3)
  .reduce(10, (a, b) -> a + b, (a, b) -> {
     log.info("combiner was called");
     return a + b;
  });

結果は前の例(16)と同じになり、ログインがなくなります。つまり、コンバイナーが呼び出されませんでした。 コンバイナーを機能させるには、ストリームを並列にする必要があります。

int reducedParallel = Arrays.asList(1, 2, 3).parallelStream()
    .reduce(10, (a, b) -> a + b, (a, b) -> {
       log.info("combiner was called");
       return a + b;
    });

ここでの結果は異なり(36)、コンバイナは2回呼び出されました。 ここで、削減は次のアルゴリズムによって機能します。ストリームのすべての要素をidentityに追加することにより、アキュムレータが3回実行されました。 これらのアクションは並行して行われています。 その結果、(10 + 1 = 11; 10 + 2 = 12; 10 + 3 = 13;)になります。 これで、コンバイナはこれら3つの結果をマージできます。 そのためには2回の反復が必要です(12 + 13 = 25; 25 + 11 = 36)。

7.2. collect()メソッド

ストリームの削減は、別の端末操作(collect()メソッド)によっても実行できます。 削減のメカニズムを指定するタイプCollector,の引数を受け入れます。 ほとんどの一般的な操作のために、すでに定義済みのコレクターが作成されています。 それらはCollectorsタイプの助けを借りてアクセスすることができます。

このセクションでは、すべてのストリームのソースとして次のListを使用します。

List productList = Arrays.asList(new Product(23, "potatoes"),
  new Product(14, "orange"), new Product(13, "lemon"),
  new Product(23, "bread"), new Product(13, "sugar"));

ストリームをCollectionCollection, ListまたはSet)に変換する:

List collectorCollection =
  productList.stream().map(Product::getName).collect(Collectors.toList());

Stringに減らす:

String listToString = productList.stream().map(Product::getName)
  .collect(Collectors.joining(", ", "[", "]"));

joiner()メソッドには、1つから3つのパラメーター(区切り文字、接頭辞、接尾辞)を含めることができます。 joiner()の使用で最も便利なこと–開発者は、区切り文字を適用するのではなく、サフィックスを適用するためにストリームが最後に到達するかどうかを確認する必要はありません。 Collectorがそれを処理します。

ストリームのすべての数値要素の平均値を処理します。

double averagePrice = productList.stream()
  .collect(Collectors.averagingInt(Product::getPrice));

ストリームのすべての数値要素の合計を処理します。

int summingPrice = productList.stream()
  .collect(Collectors.summingInt(Product::getPrice));

メソッドaveragingXX(), summingXX()およびsummarizingXX()は、それらのラッパークラス(Integer, Long, Double)と同様に、プリミティブ(int, long, double)と同様に機能します。 これらのメソッドのもう1つの強力な機能は、マッピングの提供です。 したがって、開発者は、collect()メソッドの前に追加のmap()操作を使用する必要はありません。

ストリームの要素に関する統計情報の収集:

IntSummaryStatistics statistics = productList.stream()
  .collect(Collectors.summarizingInt(Product::getPrice));

タイプIntSummaryStatisticsの結果のインスタンスを使用することにより、開発者はtoString()メソッドを適用して統計レポートを作成できます。 結果は、この1つに共通のStringになります“IntSummaryStatistics\{count=5, sum=86, min=13, average=17,200000, max=23}”.

メソッドgetCount(), getSum(), getMin(), getAverage(), getMax().を適用することにより、このオブジェクトからcount, sum, min, averageの個別の値を抽出することも簡単です。これらの値はすべて単一のパイプラインから抽出できます。

指定された機能に従ったストリームの要素のグループ化:

Map> collectorMapOfLists = productList.stream()
  .collect(Collectors.groupingBy(Product::getPrice));

上記の例では、ストリームはMapに削減され、すべての製品が価格でグループ化されています。

いくつかの述語に従って、ストリームの要素をグループに分割します。

Map> mapPartioned = productList.stream()
  .collect(Collectors.partitioningBy(element -> element.getPrice() > 15));

コレクターをプッシュして追加の変換を実行します。

Set unmodifiableSet = productList.stream()
  .collect(Collectors.collectingAndThen(Collectors.toSet(),
  Collections::unmodifiableSet));

この特定のケースでは、コレクターはストリームをSetに変換し、それから変更不可能なSetを作成しました。

カスタムコレクター:

何らかの理由でカスタムコレクターを作成する必要がある場合、最も簡単で冗長性の低い方法は、タイプCollector.のメソッドof()を使用することです。

Collector> toLinkedList =
  Collector.of(LinkedList::new, LinkedList::add,
    (first, second) -> {
       first.addAll(second);
       return first;
    });

LinkedList linkedListOfPersons =
  productList.stream().collect(toLinkedList);

この例では、CollectorのインスタンスがLinkedList<Persone>.に縮小されています

並列ストリーム

Java 8以前は、並列化は複雑でした。 ExecutorServiceForkJoinの出現により、開発者の生活は少し簡素化されましたが、特定のエグゼキュータを作成する方法、実行する方法などを覚えておく必要があります。 Java 8では、機能的なスタイルで並列処理を実現する方法が導入されました。

APIにより、並列モードで操作を実行する並列ストリームを作成できます。 ストリームのソースがCollectionまたはarrayの場合、parallelStream()メソッドを使用して実現できます。

Stream streamOfCollection = productList.parallelStream();
boolean isParallel = streamOfCollection.isParallel();
boolean bigPrice = streamOfCollection
  .map(product -> product.getPrice() * 12)
  .anyMatch(price -> price > 200);

ストリームのソースがCollectionまたはarrayとは異なる場合は、parallel()メソッドを使用する必要があります。

IntStream intStreamParallel = IntStream.range(1, 150).parallel();
boolean isParallel = intStreamParallel.isParallel();

内部的には、Stream APIは自動的にForkJoinフレームワークを使用して、操作を並行して実行します。 デフォルトでは、共通のスレッドプールが使用され、カスタムスレッドプールを割り当てる方法は(少なくとも今のところ)ありません。 This can be overcome by using a custom set of parallel collectors.

ストリームを並列モードで使用する場合、ブロック操作を避け、タスクの実行に同様の時間が必要な場合は並列モードを使用します(1つのタスクが他のタスクよりも長く続く場合、アプリのワークフロー全体が遅くなります)。

パラレルモードのストリームは、sequential()メソッドを使用してシーケンシャルモードに戻すことができます。

IntStream intStreamSequential = intStreamParallel.sequential();
boolean isParallel = intStreamSequential.isParallel();

結論

Stream APIは強力ですが、要素のシーケンスを処理するためのツールセットを理解するのは簡単です。 適切に使用すると、大量の定型コードを削減し、読みやすいプログラムを作成し、アプリの生産性を向上させることができます。

この記事に示されているほとんどのコードサンプルでは、​​ストリームは消費されないままでした(close()メソッドまたはターミナル操作は適用しませんでした)。 実際のアプリでは、don’t leave an instantiated streams unconsumed as that will lead to memory leaks.

記事に付属する完全なコードサンプルは、over on GitHub.で入手できます。