Java 8からのストリームAPIを使ったデータ処理


このブログ記事では、Java 8からStream APIを使ってデータを宣言的に処理する方法を紹介しています。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

Javaでは、コレクションと配列は2つの一般的なデータ構造であり、追加、削除、変更、クエリ、集計、統計、フィルタリングなど、多くの操作が定期的に実行されます。これらの操作はリレーショナルデータベースにも存在します。しかし、Java 8以前では、コレクションや配列を処理するのはあまり便利ではありません。

この問題は、Java 8ではStream APIと呼ばれる新しい抽象化を導入することで大幅に緩和され、宣言的な方法でデータを処理することができるようになりました。この記事では、Streamの使い方を紹介します。ストリームの性能や原理は、この記事の中心ではないことに注意してください。

ストリーム紹介

ストリームは、SQL文と同様にデータベースからデータを問い合わせることで、Javaのコレクション操作や式の高レベルな抽象化を提供します。

ストリームAPIは、Javaプログラマの生産性を大幅に向上させ、効果的でクリーンで簡潔なコードを書くことを可能にします。

処理すべき要素の集合は、パイプラインで伝送されるストリームとみなされます。これらの要素は、フィルタ、ソート、集約など、パイプラインのノードで処理することができます。

Java ストリームの特徴と利点

  • ストレージはありません。ストリームはデータ構造ではなく、データソースのビューに過ぎません。
  • ストリームは本質的に機能的なものです。ストリームに変更を加えても、データソースは変更されません。例えば、ストリームをフィルタリングしても、フィルタリングされた要素は削除されませんが、フィルタリングされた要素を含まない新しいストリームが生成されます。
  • 遅延評価。ストリームに対する操作はすぐには実行されません。ユーザーが本当に結果を必要としているときにのみ実行されます。
  • 消費可能。ストリームの要素は、ストリームの寿命の間に一度だけ訪問されます。一度トラバースされると、コンテナのイテレータのように、ストリームは無効になります。再度Streamをトラバースしたい場合は、新しいStreamを再生成する必要があります。 例を使って、Streamが何をすることができるかを見てみましょう。

先の例では、いくつかのプラスチック製のボールをデータソースとして取得し、赤いボールをフィルタリングし、それらを溶かしてランダムな三角形に変換しています。別のフィルタは小さな三角形を除去します。減力剤は、円周を合計します。

前述の図に示すように、ストリームには、ストリーム作成、中間操作、端末操作の3つの重要な操作が含まれています。

ストリーム作成

Java 8では、多くのメソッドを使用してStreamを作成することができます。

1. 既存のコレクションを使ってストリームを作成する

Java 8では、多くのストリーム関連のクラスに加えて、コレクションクラス自体も強化されています。Java 8のStreamメソッドは、コレクションをStreamに変換することができます。

List<String> strings = Arrays.asList("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis");
Stream<String> stream = strings.stream();

前述の例では、既存のリストからストリームを作成しています。また、parallelStreamメソッドは、コレクションに対して並列ストリームを作成することができます。

また、コレクションからStreamを作成することもよくあります。

2. ストリーム メソッドを使用してストリームを作成する

Streamが提供するofメソッドは、指定された要素からなるStreamを直接返すために使用することができます。

Stream<String> stream = Stream.of("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis");

前述のコードでは、of メソッドを使用してストリームを作成し、それを返しています。

ストリームの中間操作

ストリームは多くの中間操作を持ち、それらを組み合わせてパイプラインを形成することができます。各中間操作はパイプライン上のワーカーのようなものです。各ワーカーはStreamを処理することができます。中間操作は新しいStreamを返します。

以下に、一般的な中間操作の一覧を示します。

filter
filter メソッドは、指定した条件で要素をフィルタリングするために使用されます。次のコードスニペットは、filter メソッドを使用して空の文字列をフィルタリングします。

List<String> strings = Arrays.asList("Hollis", "", "HollisChuang", "H", "hollis");
strings.stream().filter(string -> ! string.isEmpty()).forEach(System.out::println);
//Hollis, , HollisChuang, H, hollis

map
mapメソッドは、各要素を対応する結果にマッピングします。以下のコードスニペットは、対応する要素の平方数を生成するために map メソッドを使用しています。

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().map( i -> i*i).forEach(System.out::println);
//9,4,4,9,49,9,25

limit/skip
Limit は、Stream の最初の N 個の要素を返します。Skip は Stream の最初の N 個の要素を放棄します。次のコードスニペットは、最初の 4 つの要素を保持するために limit メソッドを使用しています。

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().limit(4).forEach(System.out::println);
//3,2,2,3

sorted
sorted メソッドは、Stream の要素をソートします。次のコードスニペットは、sorted メソッドを使用して Stream の要素をソートします。

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().sorted().forEach(System.out::println);
//2,2,3,3,3,5,7

distinct
重複を削除するには、distinct メソッドを使用します。次のコードスニペットは、distinct メソッドを使用して要素を重複排除します。

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().distinct().forEach(System.out::println);
//3,2,7,5

次に、filter, map, sort, limit, distinct の操作を行った後の Stream がどうなるかを例と図を使って説明します。

以下にコードを示します。

List<String> strings = Arrays.asList("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis");
Stream s = strings.stream().filter(string -> string.length()<= 6).map(String::length).sorted().limit(3)
            .distinct();

次の図は、各ステップとその結果を示しています。

ストリームターミナル事業

ストリームの端末操作もStreamを返します。ストリームを目的の型に変換するにはどうすればよいのでしょうか?例えば、Stream内の要素をカウントして、そのStreamをコレクションに変換します。これを行うには、ターミナル操作が必要です。

ターミナル操作はStreamを消費し、最終的な結果を生成します。つまり、あるストリームに対して端末操作が実行された後は、そのストリームは再利用できず、そのストリームに対していかなる中間操作も許されません。そうでなければ例外が投げられます。

java.lang.IllegalStateException: stream has already been operated upon or closed

これは、「同じ川に二度は踏み込めない」ということわざの意味と同じです。

以下の表は、一般的な端末操作を示しています。

forEach
forEach メソッドは、ストリーム内の要素を繰り返し処理します。次のコードスニペットは forEach を使用して 10 個の乱数を返します。

Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

count
countメソッドは、Stream内の要素をカウントします。

List<String> strings = Arrays.asList("Hollis", "HollisChuang", "hollis","Hollis666", "Hello", "HelloWorld", "Hollis");
System.out.println(strings.stream().count());
//7

collect
コレクト操作は、様々なパラメータを受け入れ、ストリーム要素をサマリー結果に蓄積することができるリデュース操作です。

List<String> strings = Arrays.asList("Hollis", "HollisChuang", "hollis","Hollis666", "Hello", "HelloWorld", "Hollis");
strings  = strings.stream().filter(string -> string.startsWith("Hollis")).collect(Collectors.toList());
System.out.println(strings);
//Hollis, HollisChuang, Hollis666, Hollis

次に、前記の例で与えられたStreamに対する異なる端末操作の結果を示すために、フィルタ、マップ、ソート、リミット、および別個の操作が実行されたことを示す図を引き続き使用します。

次の図は、この記事で説明したすべての操作の入力と出力を示すために例を使用しています。

概要

この記事では、Java 8におけるストリームの使い方と特徴を解説します。また、この記事ではストリームの作成、ストリームの中間操作、端末操作についても解説しています。

ストリームの作成には、コレクションのストリームメソッドを使用する方法と、ストリームのofメソッドを使用する方法の2つの方法があります。

ストリームの中間演算は、ストリームを処理することができます。中間操作の入力と出力の両方がStreamです。中間操作には、フィルタ、マップ、ソートなどがあります。

ストリーム中間操作は、ストリーム内の要素をカウントしたり、ストリームをコレクションに変換したり、ストリーム内の要素を反復処理したりするなど、ストリームを何らかの他のコンテナに変換することができます。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ