エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

Java8 Stream APIの基本(4) - ストリームの内部

先日の記事ではStreamの基本的な生成処理について述べた。

今回はStreamの内部処理で扱われるSpliteratorについて解説する。

Spliterator

Spliteratorは分割可能なイテレータであり、Streamの低レベルAPI用のインターフェースである(名前の由来はsplitとiteretorの合成と思われる)。コレクションの繰り返しにはIteratorが使われるように、Streamの要素の走査は内部的にSpliteratorを用いている。
Spliteratorには 要素を2つに分割する trySplitメソッドがあり、並列ストリームで各スレッドに要素を分割する方法として使用されている。 また中間・終端走査や、並列処理を効率化するヒントとして、characteristics(性質)、 estimateSize(サイズ)等の属性を持つ(characteristicsについては後述する)。

Spliteratorを作成することで、以下のような任意のデータ構造をストリームにすることができる。

  • 2つのListなど、複数のデータから1要素ずつ要素を取得し、1つにまとめるストリーム(zipメソッドの実現。次回解説)
  • 標準入力などから1文字ずつ読み取るストリーム
  • JDBCResultSetから1レコードずつフェッチするストリーム(遅延ローディングの実現)

Spliteratorを作成するには、Spliteratorを実装したクラスを作成する方法と、Iteratorを実装したクラスから構築する方法の2通りがある。
後者の方が容易なため、この記事では後者の方法について解説する。

IteratorからSpliteratorの生成

Spliteratorsクラスは、Spliteratorに関するユーティリティクラスであり、Iteratorやコレクション等からSpliteratorを生成する方法を提供する。 IteratorからSpliteratorを生成するメソッドは以下の2つがある。

  • spliterator(Iterator<? extends T> iterator, long size, int characteristics)
  • spliteratorUnknownSize(Iterator<? extends T> iterator, int characteristics) (性質SIZEDの指定は無効)

両者の違いはサイズを指定するかどうかであり、予め要素の個数がわかるなら前者、Readerの読み込みなどサイズが不明な場合は後者を用いる。
現在のStreamの実装では、サイズが有限である場合に並列処理が最適化されるようになっている。そのため、サイズが予めわかる場合は前者を用いた方が良い。
(当方で検証した結果、無限ストリームの場合、要素がおおよそ1000以下の場合では並列ストリームであっても単一スレッドで処理が行われた。一方、有限ストリームの場合は要素数に関係なく複数スレッドで並列処理が実行された。)

Spliteratorから、Streamを生成するには、StreamSupport#stream を用いる。 内部的には上記のストリーム生成処理はほぼ全て、これらのクラスを用いている。

characteristics(性質)

characteristicsはストリームを生成したデータがどのような性質を持っているかを示す。
元のコレクションにおいては、Listには順序があり、 Setには要素の重複が無い、などの性質があるが、Streamに変換された時点で生成前のコレクションの型の情報は消えてしまう。このため、生成前のコレクションの性質を保持するためのにcharacteristicsという属性を持たせている。
(実際上記で紹介した各Streamの生成メソッドは、生成する型に応じた性質を持ったSpliteratorを内部で生成している)。
characteristicsは、Spliteratorに定数として定義されたint値だが、2進数のフラグとして複数の性質を持てるようになっている。

代表的な性質を以下に示す。

性質 意味 主な例
SIZED サイズが有限である List, Set, Array, Stream#of
ORDERED 順序が決まっている List,Array, Stream#iterate, Stream#generate
SORTED 要素がソート済み TreeSet
DISTINCT 要素が全て一意 Set
NONNULL nullの要素がない

性質の値によって、Streamは処理の効率化を行う場合がある。 例えば、以下のようにListに無理やり本来とは異なる性質を与えると、本来行われるべき処理が行われないことがわかる。 (検証のために行っているので、実際に実装してはならない。)

// ソート済みでもユニークでもないリスト
List<Integer> list = Arrays.asList(1,2,2,5,4,3);

// 重複除去、ソートが行われ、 12345 が結果となる。
list.stream().sorted().distinct().forEach(System.out::print);
System.out.println();

// ソート済み、ユニークの性質を与えたStreamを生成する。
Stream<Integer> st = StreamSupport.stream(
        Spliterators.spliterator(list.iterator(), list.size(), Spliterator.SORTED | Spliterator.DISTINCT)
        , false);
// 入力値のまま、122543 と表示される。 
st.sorted().distinct().forEach(System.out::print);

まとめ

先日の記事に続き、ストリーム内部で使われているSpliteratorを解説した。

次回は、Spliteratorを用いた独自のStream作成例について解説する。

[前多 賢太郎]