Java 8 Stream APIチュートリアル

1概要

この詳細なチュートリアルでは、作成から並列実行まで、Java 8 Streamsの実用的な使い方を説明します。

この資料を理解するためには、読者はJava 8(ラムダ式、 Optional、 メソッド参照)およびStream APIについての基本的な知識が必要です。これらのトピックについて詳しくない場合は、以前の記事、リンク:/java-8-new-features[Java 8の新機能]およびリンク:/java-8-streams-Introductionを参照してください。 Java 8 Streams]

2ストリーム作成

さまざまなソースのストリームインスタンスを作成する方法はたくさんあります。

いったん作成されると、インスタンスはそのソースを変更しないため、単一のソースから複数のインスタンスを作成することができます。

2.1. 空のストリーム

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

Stream<String> streamEmpty = Stream.empty();

多くの場合、 empty() メソッドが作成時に使用され、要素のないストリームに対して null が返されないようにします。

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

2.2. Collection のストリーム

ストリームは任意のタイプの Collection Collection、List、Set )からも作成できます。

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

2.3. 配列のストリーム

配列はStreamのソースにもなります。

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

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

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

2.4. Stream.builder()

  • builderが使用されているとき ステートメントの右側に希望の型を追加指定する必要があります。

Stream<String> streamBuilder =
  Stream.<String>builder().add("a").add("b").add("c").build();
  • 2.5 Stream.generate() **

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

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

上記のコードは、値 - "element" を持つ10個の文字列のシーケンスを作成します。

2.6. Stream.iterate()

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

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

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

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

Java 8では、 int、long 、および double. の3つのプリミティブ型からストリームを作成することができます。 Stream <T> は一般的なインタフェースであり、ジェネリック型の型パラメータとしてプリミティブを使用する方法はありません。作成された: 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以降、https://docs.oracle.com/javase/8/docs/api/java/util/Random.html[ 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();

次の例は、指定された RegEx に従って String をサブストリングに分割します。

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

2.9. ファイルの流れ

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

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

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

3. ストリーム の参照

中間操作のみが呼び出される限り、ストリームをインスタンス化し、それにアクセス可能な参照を持たせることは可能です。

端末操作を実行すると、ストリームにアクセスできなくなります _. _

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

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

しかし、端末操作を呼び出した後に同じ参照を再利用しようとすると、__IllegalStateExceptionが発生します。

Optional<String> firstElement = stream.findFirst();

IllegalStateException RuntimeException なので、コンパイラは問題について通知しません。そのため、 Java 8 ストリームは再利用できないことを覚えておくことは非常に重要です。

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

そのため、以前のコードを適切に機能させるためには、いくつか変更を加える必要があります。

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

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

データソースの要素に対して一連の操作を実行し、それらの結果を集約するには、3つの部分が必要です。 source 中間操作 端末操作

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

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

複数の変更が必要な場合は、中間操作を連鎖させることができます。現在の Stream <String> のすべての要素を最初の数文字のサブストリングで置き換える必要もあるとします。これは、 skip() メソッドと map() メソッドを連鎖させることによって行われます。

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

ご覧のとおり、 map() メソッドはラムダ式をパラメータとして取ります。ラムダについてもっと知りたいのなら、チュートリアルリンク/java-8-lambda-expressions-tips[ラムダ式と関数型インタフェース:Tips and Best Practices]を見てください。

ストリーム自体には価値がありません。ユーザーが興味を持っているのは、端末操作の結果です。これは、ストリームのすべての要素に適用される何らかのタイプの値またはアクションです。 ** ストリームごとに使用できる端末操作は1つだけです。

ストリームを使用する最も便利で便利な方法は、 ストリームパイプライン、つまりストリームソース、中間操作、および端末操作のチェーンです。 次に例を示します。

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

5遅延呼び出し

  • 中間操作は怠惰です。 これは、 端末操作の実行に必要な場合にのみ呼び出されることを意味します。

これを実証するために、__wasCalled()というメソッドがあるとします。これは、呼び出されるたびに内部カウンタをインクリメントします。

private long counter;

private void wasCalled() {
    counter++;
}

オペレーション filter() からメソッドwas __Called() __を呼び出しましょう。

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

3つの要素からなるソースがあるので、メソッド filter() は3回呼び出され、 counter 変数の値は3になると想定できます。そのため、 filter() メソッドは一度も呼び出されませんでした。その理由 - は端末操作に欠けています。

map() 操作と端末操作 - __findFirst()を追加して、このコードを少し書き直してみましょう。ロギングを使用してメソッド呼び出しの順序を追跡する機能も追加します。

Optional<String> 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つの要素で満たされます。そのため、この特定の例では、lazy呼び出しによって2つのメソッド呼び出し(1つは filter() 用、もう1つは__map()用)を避けることができました。

6. 実行順序

パフォーマンスの観点からは、** 正しい順序は、ストリームパイプラインでのチェーン操作の最も重要な側面の1つです。

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() は一度だけ呼び出されます。

long size = list.stream().skip(2).map(element -> {
    wasCalled();
    return element.substring(0, 3);
}).count();
  • ストリームのサイズを縮小する中間操作は、各要素に適用される操作の前に配置する必要があります。したがって、s____kip()、filter()、distinct()などのメソッドを使用してください。ストリームパイプラインの最上位。

7. ストリーム削減

APIには、 count()、max()、min()、sum()など、ストリームを型またはプリミティブに集約する多くの端末操作がありますが、これらの操作は事前定義された実装に従って機能します。そして開発者がStreamの縮小メカニズムをカスタマイズする必要がある場合はどうなりますか? これを可能にする2つのメソッド - reduce() メソッドと collect()__ ** メソッドがあります。

7.1. reduce() メソッド

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

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

  • accumulator - ** 要素の集約の論理を指定する関数。アキュムレータは削減の各ステップに対して新しい値を作成するので、新しい値の量はストリームのサイズに等しく、最後の値だけが役に立ちます。これはパフォーマンスにとってあまり良くありません。

  • combiner - ** アキュムレータの結果を集計する関数。

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

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

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

還元 = 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<Product> productList = Arrays.asList(new Product(23, "potatoes"),
  new Product(14, "orange"), new Product(13, "lemon"),
  new Product(23, "bread"), new Product(13, "sugar"));
  • ストリームを Collection Collection、List 、または Set )に変換する:**

List<String> 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 )と同様に機能します。これらのメソッドのもう一つの強力な機能はマッピングを提供することです。したがって、開発者は collect() メソッドの前に追加の map() 操作を使用する必要はありません。

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

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

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

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

指定された関数に従ってストリームの要素をグループ化します。

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

上記の例では、ストリームは Map に縮小されています。これは、すべての商品をその価格でグループ化したものです。

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

Map<Boolean, List<Product>> mapPartioned = productList.stream()
  .collect(Collectors.partitioningBy(element -> element.getPrice() > 15));
  • コレクターを押して追加の変換を実行します。**

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

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

  • カスタムコレクター:**

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

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

LinkedList<Product> linkedListOfPersons =
  productList.stream().collect(toLinkedList);

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

パラレルストリーム

Java 8以前は、並列化は複雑でした。リンクの出現:/java-executor-service-tutorial[ ExecutorService ]と ForkJoin 開発者の生活を少し簡略化しましたが、それでも特定のexecutorを作成する方法を覚えておくべきです。 、それを実行する方法など。 Java 8では、機能的スタイルで並列処理を実現する方法が導入されました。

このAPIでは、パラレルモードで操作を実行するパラレルストリームを作成できます。ストリームのソースが Collection または array の場合、 parallelStream() メソッドを使用してそれを実現できます。

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

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

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

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

並列モードでストリームを使用する場合は、操作のブロックを避け、タスクの実行時間が同程度の時間が必要な場合は並列モードを使用します(あるタスクが他のタスクよりもはるかに長く続くと、アプリケーションのワークフロー全体が遅くなる可能性があります)。

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

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

結論

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

この記事で示したコードサンプルの大部分では、ストリームは消費されていません(__close()メソッドや端末操作は適用していません)。実際のアプリでは、インスタンス化されたストリームをメモリリークの原因となるため、消費されないようにしてください。

記事に付随する完全なコードサンプルが利用可能です。 over on GitHub。