技術情報棚卸し(平日限定)

todoa2cの技術情報棚卸しです。平日限定ってことはアレだ。言わせんな恥ずかしい。

Java 8リリース & java.util.streamのお勉強

ようやくJava 8がリリースされましたね。 正直Java飽きたと思っていたのですが、Java 8はかなり熱い機能が満載です。 特に関数型ちっくに書けるのは大きいですね。

とりあえず練習のため、1から100までに対するFizzBuzzを、今時点で知る限りの Java 8の機能を使って書いてみました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.stream.IntStream;

public class Main {
  public static String fizzbuzz(int x) {
      if (x % 15 == 0)
          return "FizzBuzz";
      else if (x % 3 == 0)
          return "Fizz";
      else if (x % 5 == 0)
          return "Buzz";
      else
          return String.valueOf(x);
  }

  public static void p(String fb) {
      System.out.println(fb);
  }

  public static void main(String[] args) {
      IntStream.range(1, 101).boxed().map(Main::fizzbuzz).forEach(Main::p);
  }
}

もはや今までに見てきたJavaとは思えない書き方ですね。 特にmain()部分は1行で色々やっているので、解説を書いていきますが、 その前に、そもそもStreamってなに?ということで、Streamについて調べました。 以下、java.util.stream.Streamインターフェースの最初の一文の引用です。

A sequence of elements supporting sequential and parallel aggregate operations.

つまり、直列処理も並列処理も行うことができる、連続したデータを集めたもの、のようです。 今までのコレクションとの最大の違いは、並列処理も行うことができる、という部分でしょうね。 また、関数型言語のような高階関数を多数用意しているところも特徴です。

Streamが何となく雰囲気分かった気がしたところで、では1行ずつ見てみましょう。 ちなみに、「模様です」と書いている部分は、まだ私自身の調査が及んでない部分です。

1
IntStream.range(1, 101)

IntStreamはprimitiveなint専用のStreamに関するインターフェース。 IntStream.range(1, 101)により、1から100までのStreamを生成します。 (IntStream.rangeClosed(1, 100)でもOKです)

1
2
IntStream.range(1, 101)
  .boxed()

.boxed()でprimitiveなintのStreamをStream<Integer>に変換。 Pipelineにより、Stream内のデータが必要になった時点で変換する模様です。 要するに一括変換ではない模様。

1
2
IntStream.range(1, 101).boxed()
  .map(Main::fizzbuzz)

map()にはFunctionを渡します。 Function<T,R>は、型Tの入力を受け取り型Rの結果を返す関数インターフェース。 map()は、型TのStreamを、引数に渡した関数を用いて型Rのストリームを変換(生成)します。

今回map()に渡すFunctionはMainクラスのfizzbuzzメソッド。 staticメソッドに対しては、特別にMain::fizzbuzzと書くことができるようになりました。 この書き方をすることで、fizzbuzzメソッドはFunctionの匿名クラスに変換される模様です。

map()の結果として、Stream<String>で、中身はFizzBuzzのStreamが生成されたことになります。

1
2
IntStream.range(1, 101).boxed().map(Main::fizzbuzz)
  .forEach(Main::p);

forEach()にはConsumerを渡します。 Consumer<T>は型Tの入力を受け取り何も返さない手続き(何も返さないから関数ではない)インターフェース。 forEach()は、型TのStreamを用いて何かしらの処理をしますが、結果は返しません。 今回のように、標準出力などの副作用を行う場合に使うことになるかと思います。

map()が返した型はStream<String>なので、Main::pの引数はStringである必要があります。 うっかりMain::pの引数をIntegerなどStringと互換のない型にした場合、 型が違うとコンパイルエラーを返してくれるため、型で悩むことがなくなります。

(System.out::println と書いてもOKですが、型の説明のためにわざわざMain::pを定義しました)。

これでmain文の難解な1行を読み解くことが出来ました(消化不良もあるかと思いますが…)。

ちなみに、Streamの説明のところで、並列処理もOKという話をしましたが、 このFizzBuzzを並列処理しようとした場合、下記のように書けばOKです。

1
2
3
IntStream.range(1, 101).boxed()
  .paraparallel()   // ここで並列処理化している
  .map(Main::fizzbuzz).forEach(Main::p);

直列処理の場合は、1, 2, Fizz, 4, …と表示されていたのですが、 並列処理にすることにより、順番がバラバラになるのが分かるかと思います。 これはmap()が、その前のStreamが並列化されたことに伴い、 順不同で並列にデータが来るようになったためと考えられます。

結果を集計するような場合にはparallelは強力ですが、順序が重要な場合には parallelは使えないと考えたほうがよいかもしれません。 ちなみに集計には、Streamにcollect(), min(), max()が定義されていました。 最大値や最小値を、複数CPUを使って探してくれるのは、何か胸が熱くなりますね(?)。

さて、いつから実戦投入できるようになるかな?それが一番の問題ですね!

Comments