倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

小川のせせらぎもやがてはうねる奔流に Stream インターフェース (2) - terminal operators part 2 -

Java の Stream インターフェースに定義されているメソッドを見ていくシリーズ(目次)。 今回は java.util.stream.Stream インターフェースに定義されている terminal operator (終端演算子) の残りを見ていきます。 どれも Stream の要素を集めた「コンテナ」風のオブジェクトを返します。 扱うメソッドは以下の4つ:

Iterator<T> iterator()

Object[] toArray()
<A> A[] toArray(IntFunction<A[]> generator)

Spliterator<T> spliterator()

Java 8 から新たに導入された java.util.Spliterator なるインターフェースがあります。 これは後ほど。

iterator() メソッド

まずは java.util.Iterator を返すメソッド iterator():

Iterator<T> iterator()

このメソッド自体は特に問題はないかと思います。 ただし Java 8 からは、Iterator インターフェースにデフォルト・メソッド forEachRemaining(Consumer) が新たに定義されました*1。 また、remove() メソッドはデフォルト・メソッドになりました。 デフォルトでは UnsupportedOperationException が投げられます:

package java.util;

pubilc interface Iterator<E>{
    boolean hasNext();
    E next();

    default void forEachRemaining(Consumer<? super E> action);
    default void remove();
}

使い方はこんな感じ:

Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure");

// iterator()
Iterator<String> ite = stream.iterator();
ite.forEachRemaining(System.out::print);    // 「JavaGroovyScalaClojure」と表示

この場合 Iterator#forEachRemaining() を使うより Stream#forEach() を使えばいいんですけどネ。 まぁ、それはともかくとして、Stream は Iterator を返す iterator() メソッドがあるのに java.lang.Iteratable インターフェースを拡張しているわけではないので、拡張 for 文に使うことはできません:

// こんなことはできない    	
for(String s : stream)
    System.out.println(s);

別にできるようにしといてくれてもいいんじゃね? 昔のインターフェースで新しい Stream インターフェースを汚染したくなかったのかな? それならそれで言語拡張して(配列が Iterable でないのに拡張 for 文で使えるように) Stream もそれ自体で拡張 for で使えるようにしとくとかならんかな。 出来なくて困るというわけでもないけど・・・

toArray() メソッド

次は Stream を配列に変換するメソッド:

Object[] toArray()
<A> A[] toArray(IntFunction<A[]> generator)

1つ目の toArray() はいいでしょう。 2つ目の toArray() は配列のインスタンス生成が Generic にできないためにその部分だけ使用者に書かせようというメソッド。 IntFunction<T> は int 値を引数にとり T オブジェクトを返す apply(int) : T メソッドを抽象メソッドとして持つ関数インターフェースです。 使い方はこんな感じ:

Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure");

// asArray()
String[] langs = stream.toArray(n -> new String[n]);    // 要素数 (int) から配列オブジェクトを生成
// String[] langs = stream.toArray(String[]::new);    // 配列のコンストラクタ参照を使う方が普通っぽい

for(String s : langs)
    System.out.print(s);
// 「JavaGroovyScalaClojure」と表示

使い方はなんてことないと思いますが、toArray() に渡す引数がほとんど決まりきった定型文なのでもうすこし何とかならないかと思うんですが・・・ 互換性の観点からはしかたないのかなぁ。

spliterator() メソッド

最後は java.util.Spliterator オブジェクトを返す spliterator() メソッド。 まずは Spilterator インターフェースの定義を見てみましょう:

package java.util;

public interface Spliterator<T>{

    static int CONCURRENT;
    static int DISTINCT;
    static int IMMUTABLE;
    static int NONNULL;
    static int ORDERED;
    static int SIZED;
    static int SORTED;
    static int SUBSIZED;

    int characteristics();
    long estimateSize();
    boolean tryAdvance(Consumer<? super T> action);
    Spliterator<T> trySplit();

    default void forEachRemaining(Consumer<? super T> action);
    default long getExactSizeIfKnown();
    default boolean hasCharacteristics(int characteristics);
    default Comparator<? super T> getComparator();
}

「spliterator」は見るからに「split + iterator」からきた造語なんでしょうね。 trySplit() メソッドによって2つの spliterator(自分自身の spliterator と trySplit() の返り値の spliterator)に分割するってのが使い方の胆なんでしょう。 これは後でサンプルコードで試してみます。

Spliterator インターフェースには Iterator と同じように forEachRemaining(Consumer) メソッドが定義されているので、このメソッドによって要素を列挙するのが通常の使い方なんでしょう。 Spliterator は Stream 同様、 Iterable を拡張しているワケではないので拡張 for 文に使ったりは出来ません。

ちょっと気になるのが、Spliterator の特性 (characteristics) を表す定数が int 型で定義されているところ。 普通に enum (と EnumSet)使おうよ・・・ なんか実装している人間が最近の Java に疎いんじゃないかと不安になるのは拙者だけ? まぁ、正式リリースまでに変更される可能性もないではないので、これはおいといて、サンプルコードを見ていきましょう:

import java.util.stream.Stream;
import java.util.Spliterator;
    	
Stream<String> stream = Stream.of("Java", "Groovy", "Scala", "Clojure");

// spliterator()
Spliterator<String> spl1 = stream.spliterator();
Spliterator<String> spl2 = spl1.trySplit();    // Spilterator を分割
  	    
spl1.forEachRemaining(System.out::print);    // 「ScalaClojure」 と表示。 後半分
System.out.println();
spl2.forEachRemaining(System.out::print);    // 「JavaGroovy」と表示。 前半分

spliterator() によって返される Spliterator オブジェクトは、元の Spliterator の前半分を返します。 spliterator() を呼び出された後は、元の Spliterator は後ろ半分が残った Spliterator になります。

素数が無限個ある場合は spliterator() の返り値の Spliterator オブジェクトは1024個(0番目~1023番目)の要素を持っているようです。 1024番目以降は元の Spliterator オブジェクトに含まれています:

Stream<Integer> stream = Stream.generate(new Supplier<Integer>(){
    private int count = 0;

    public synchronized Integer get(){
        return this.count++;
    }
});

Spliterator<Integer> spl1 = stream.spliterator();
Spliterator<Integer> spl2 = spl1.trySplit();
  	    
writeDown(spl1, "[Spl 1] ");
writeDown(spl2, "[Spl 2]      ");

// wrteDown() メソッド
public static <T> void writeDown(Spliterator<T> spl, String prefix)
        throws InterruptedException{
    Consumer<T> cons = (T t) -> {
        try{
            Thread.sleep(100);

        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(prefix + t);
    };

    Runnable run = () -> spl.forEachRemaining(cons);
    new Thread(run).start();
}
[Spl 2]      0
[Spl 1] 1024
[Spl 2]      1
[Spl 1] 1025
[Spl 2]      2
[Spl 1] 1026
[Spl 2]      3
[Spl 1] 1027
[Spl 2]      4
[Spl 1] 1028
[Spl 1] 1029
[Spl 2]      5
[Spl 2]      6
[Spl 1] 1030
[Spl 1] 1031
[Spl 2]      7

Spliterator は実質的に Stream を分割する型なので、タスクの並行実行などに重要な役割を果たすようになるかも知れませんが、それなら関数型言語のように Stream 自体を分割できるようにならんかったんかな・・・

次回は short-circuiting terminal operators の予定。

追記
Stream#toArray() メソッドの引数として配列のコンストラクタ参照を渡す部分を追加しました。

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

オブジェクト指向における再利用のためのデザインパターン

オブジェクト指向における再利用のためのデザインパターン

*1:以前のビルドでは forEachRemaining() ではなく forEach() だったかと。