倭マン's BLOG

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

小川のせせらぎもやがてはうねる奔流に Stream インターフェース (8) - プリミティブ型ストリーム -

Java の Stream インターフェースに定義されているメソッドを見ていくシリーズ(目次)。 前回までで Stream インターフェースに定義されているメソッドは大体見たので、今回はプリミティブ型のストリーム(以下、プリミティブ型ストリーム)を見ていきます。 プリミティブ型ストリームには

  • IntStream
  • LongStream
  • DoubleStream

(どれも java.util.stream パッケージ)のインターフェースの3つがあります。 遂に全プリミティブ型をサポートするのを諦めた感じでw java.util.function パッケージのインターフェース群も int, long, double の3つのプリミティブ型しかサポートしてしてませんでしたね。 まぁ、boolean とか char とか必要になったら、boxing によってラッパー型扱えばいいだけですけどね。

これらのプリミティブ型ストリームに定義されているメソッドは、ほとんどが Stream インターフェースに定義されているものの焼き直しです。 例えば Stream インターフェースに定義されている以下のメソッドは

<R> Stream<R> map(Function<? super T,? extends R> mapper)
Optional<T> reduce(BinaryOperator<T> accumulator)

IntStream では

IntStream map(IntUnaryOperator mapper)
OptionalInt reduce(IntBinaryOperator op)

という風に型パラメータに int を代入したかのようなメソッドになっています(型としては継承関係などのない完全に別の型ですが):

  • Function<T, R> → IntUnaryOperator*1
  • BinaryOperator<T> → IntBinaryOperator
  • Optional<T> → OptionalInt

これらのメソッドの使い方は Stream インターフェースの場合と同じなので、というか型パラメータがないので型推論機能と合わせて使えばむしろ Stream よりもわかりやすいので、説明は不要かと思います。 よって、プリミティブ型ストリームのメソッドの中で Stream インターフェースに対応するものがないメソッドを見ていけば充分でしょう。 以下では、プリミティブ型ストリームの中でも特に IntStream を見ていきます。

プリミティブ型ストリーム特有のメソッド

プリミティブ型ストリーム、特に int 値のストリームである IntStream には、以下のようなメソッドが定義されています:

package java.util.stream;

public interface IntStream{

    ...    // Stream インターフェースに定義されているのと同様のメソッド

    // static ファクトリメソッド
    static IntStream range(int startInclusive, int endExclusive);
    static IntStream range(int startInclusive, int endExclusive, int step);

    // Map 処理
    Stream<Integer> boxed();
    LongStream longs();
    DoubleStream doubles();
    <U> Stream<U> mapToObj(IntFunction<? extends U> mapper);

    // Reduce 処理
    int sum();
    OptionalDouble average();
    IntSummaryStatistics summaryStatistics();
}
  • static ファクトリメソッド
  • Map 処理
  • Reduce 処理

の3つに分けてそれぞれを見ていきましょう。

static ファクトリメソッド

プリミティブ型ストリームには範囲を表すストリームを生成する range() メソッド が static メソッドとして定義されています。 何気にプリミティブ型ストリームのなかで一番多用するメソッドなんじゃないかと。 range() メソッドには

  • 始値(含む)と終了値(含まない)を指定する
  • 始値(含む)と終了値(含まない)と刻み幅を指定する

という2つのバージョンがあります。 終了値は含まないことに気を付ければ使い方は簡単:

// range()
IntStream is0 = IntStream.range(0, 10);
is0.forEach(System.out::print);    // 「0123456789」 と表示

IntStream is1 = IntStream.range(0, 10, 2);
is1.forEach(System.out::print);    // 「02468」 と表示

IntStream ってこれのためにあるって思ってもいいんじゃないかってくらいかもw

Map 処理

次は Map 処理。 プリミティブ型から別の(プリミティブ型)ストリームを返すメソッドです。

  • boxed() はラッパー型の Stream オブジェクトに
  • longs() は LongStream オブジェクトに
  • doubles() は DoubleStream オブジェクトに
  • mapToObj() は Stream オブジェクトに

それぞれ変換します。 ちなみに LongStream, DoubleStream には IntStream オブジェクトに変換する ints() のようなメソッドは定義されていません*2。 使い方はこんな感じ(ちょっと込み入ったサンプルですが):

// boxed()
assert IntStream.range(0, 10)
                .boxed()    // これがないと次の行で i.toString() が怒られる
                .map(i -> i.toString())
                .collect(Collectors.toStringJoiner("-")).toString()
            .equals("0-1-2-3-4-5-6-7-8-9");

// longs()
assert Integer.MAX_VALUE == 2147483647;

assert IntStream.range(1, 12).reduce(1, (i1, i2) -> i1 * i2) == 39916800;    // 1から12 までの積
assert IntStream.range(1, 13).reduce(1, (i1, i2) -> i1 * i2) == 479001600;
assert IntStream.range(1, 14).reduce(1, (i1, i2) -> i1 * i2) == 1932053504;    // 間違い! 桁落ち

assert IntStream.range(1, 12).longs().reduce(1, (i1, i2) -> i1 * i2) == 39916800L;
assert IntStream.range(1, 13).longs().reduce(1, (i1, i2) -> i1 * i2) == 479001600L;
assert IntStream.range(1, 14).longs().reduce(1, (i1, i2) -> i1 * i2) == 6227020800L;    // 正しい!

// doubles()
assertDouble(IntStream.range(0, 10).map(i -> i/2).sum(), 20);    // 整数の割り算
assertDouble(IntStream.range(0, 10).doubles().map(d -> d/2).sum(), 22.5);    // 小数の割り算

// mapToObj()
String s = "Java 8 Project Lambda";
// 文字列を1文字ごとの Stream に
Stream<String> ss = IntStream.range(0, s.length()).mapToObj(i -> s.substring(i, i+1));
ss.forEach(System.out::println);

/** double 値のアサーション */
public static void assertDouble(double d0, double d1){
    assert Math.abs(d0 - d1) < 0.001;
}

最後の mapToObj() を使うサンプルでは、以前に flatMap() メソッドを使うサンプルで使った文字列を1文字ごとの文字(列)を要素とする Stream オブジェクトへ変換する方法を簡単に書く方法を示しています。

Reduce 処理

最後は Reduce 処理。 数値のストリームなので和や平均を計算するメソッドが定義されています。 ストリームだと、1度要素を列挙してしまうともう戻れないので、Summary statistics (要約統計量、代表値)を一括計算して結果をまとめた java.util.IntSummaryStatistics オブジェクトを返す summaryStatistics() メソッドもあります(LongSummaryStatistics, DoubleSummaryStatistics もありま~す)。 IntSummaryStatistics クラスの定義はこんな感じ:

package java.util;

public Class IntSummaryStatistics implements IntConsumer{

    // Consutructor
    public IntSummaryStatistics()

    // Methods
    void accept(int value);
    void combine(IntSummaryStatistics other);

    long getCount();
    long getSum();
    double getAverage();
    int getMax();
    int getMin();

    String toString();
}

計算できるのは

  • 素数
  • 平均
  • 最大値
  • 最小値

だけです。 せめて分散くらい計算できた方が・・・ まぁともかく、これらを使ったサンプルコードを見てみましょう:

final int n = 100;

// sum()
assert IntStream.range(1, n+1).sum() == n*(n+1)/2;    // 初項1、公差1、項数 n の等差数列の和

// average()
assertDouble(IntStream.range(1, n+1).average().getAsDouble(), (n+1)/2.0);

// summaryStatistics()
IntSummaryStatistics stat = IntStream.range(1, n+1).summaryStatistics();
assert stat.getCount() == n;
assert stat.getSum() == n*(n+1)/2;
assertDouble(stat.getAverage(), (n+1)/2.0);
assert stat.getMin() == 1;
assert stat.getMax() == n;
//println(stat.toString());

/** double 値のアサーション */
public static void assertDouble(double d0, double d1){
    assert Math.abs(d0 - d1) < 0.001;
}
  • IntStream#average() メソッドの返り値は(IntStream でも)OptionalDouble なのに注意。 まぁ、平均は整数とは限らんよね。

Java 8 ea-b90 だと、IntSummaryStatistics#toString() を呼び出すと例外が投げられるんですけど・・・ なんかフォーマット関連の。

プリミティブ型ストリームで一番重要なのはやはり range() メソッドではないかと。 数値だと計算ができるので統計量を取得するメソッドも定義されてますが、かなりしょぼいサポートなので別ライブラリの登場を待つのが吉かと。

これで大体ストリーム関連のインターフェースは終わり。 次は惰性で Collectors のメソッドでも見ていきましょうかね。

プログラミングHaskell

プログラミングHaskell

*1:Function<int, int> みたいな感じ

*2:同様に DoubleStream には longs() も定義されてません。