JavaSE8は、なんといってもストリームAPIとラムダ式。

いろいろとできることが増えたので
どこから手をつければよいのか困ってしまうが、
導入経緯を探っていくと理解しやすい。

ここでは、ストリームAPIやラムダ式の周辺が
どんな理由で必要になったかを考える。

並列処理にはそれが必要だった

導入経緯をまとめると、次のような話。
(個人の主観的なまとめです。詳細はご自身でお確かめ下さい。)

  1. CPUがマルチコア化しているのに
    プログラム側が対応しきれていないのはもったいない。
  2. 並行処理用のAPI(Concurrency Utilities)があるじゃないか。
  3. 粒度の大きな処理は問題ないが、
    粒度の小さな処理を並列化する場合には使いにくい。
  4. じゃあ、イテレータに注目しよう。
    反復されている処理が、簡単に並列化できればうれしい。
    イテレータを改良すればなんとかなるのでは。
  5. でも、今の外部イテレータってを崩さずに並列化するのは結局不便。
  6. じゃあ、内部イテレータをできるようにしよう。→ストリームAPI
  7. 内部イテレータ用のメソッドをコレクションに加えたら
    以前のコードが軒並みコンパイルエラーになってしまう。→defaultの導入
  8. 内部イテレータができるようになっても
    処理を書くときに無名クラスを書いてるようじゃ、
    ごちゃごちゃしてしまう。
  9. じゃあ、省略して処理を書く記述方法を追加しよう。→ラムダ式

ざっとこんなかんじ。

CPUのマルチコア化の流れが大き後押ししたかたち。
Javaでもラムダ式をできるようにしようって人は昔からいたけど
賛成派と反対派は五分五分で勢いは少なかった。

基本的にConcurrency Utilitiesで大きな処理は並行化できているので、
次は小さな処理ということになる。

ちなみにここでいう粒度の小さい処理とは、1~数行の処理ぐらいのこと。
粒度の大きい処理とは、10行以上の大きめのメソッド1つ分ぐらいの
まとまった処理ってこと。

このときイテレーションに注目をしているが、これも自然な流れ。
ループ内の繰り返される処理をスレッドで分担して実行できればという発想で、
これを簡単に行うことができる言語はいくつもある。

JavaSE5からのConcurrency Utilitiesは確かに強力だけど
よほどの遅い処理ではない限りいちいちfor文内の処理1行(粒度の小さい処理)を
並列化しようという発想はならない。

Executorの選定、同期はどうするか、しっかり例外処理できているかなど
並列処理で考えることは多いから気合いを入れたところしかできないのが現実的。

でも、並列処理に関する部分をストリームAPIが受け持って
処理をラムダ式で簡素に書ければ
手軽にイテレータレベルの小さい処理でも
並列化したいな(できる)ってことになるはず。

ってことでConcurrency Utilitiesだけじゃなくて
「ストリームAPI」や「default」や「ラムダ式」が
必要ってことになっていく。

並列処理をライブラリで行うための内部イテレーション

内部イテレーションにする一番の目的はこれになる。

これについては
内部と外部の違いを見ていくとわかりやすい。

イテレーションといえば、Java7まではfor文。
これは外部イテレーション。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
for (int number : numbers) {
    System.out.println(number);
}

一方、Java8からのストリームAPIを利用したイテレーション。
これは内部イテレーション。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream().forEach(number -> {
    System.out.println(number);
});

内部イテレーションと外部イテレーションの違いは、
反復処理を実際に行うのがイテレーションの外側か内側かの違い。

外部イテレーションは
利用者が反復処理を行っている。

一方、内部イテレーションは
処理を渡しているだけで利用者は反復処理はしていない。
反復処理をしているのはイテレーションの内側。
つまり、ライブラリが反復処理をしてくれている。

この違いが大きい。
利用者側は反復処理を書く責任から逃れることになり、
内部イテレーションではライブラリに請け負ってもらえる。

並列処理をライブラリ側でしっかり用意してくれれば、
使う人が直接並列処理を書かなくても、
ライブラリ側で並列処理することが可能ってことを意味する。

外部イテレーションのままではどうがんばっても、
使う人が反復処理の責任から逃れられない。

イテレーションを渡して反復処理をしてもらうような形でも
スッキリはしない。

よって、並列処理をライブラリで行うには
内部イテレーションが欠かせない。

この内部イテレーションを実際に行ったりインタフェースを提供するのが
ストリームAPI。

このストリームAPIを受け入れるために、
Collectionクラスではstreamオブジェクト生成用に
stream()メソッド等が追加された。

ストリームAPIと互換性を保つためのdefault

内部イテレーションを実現するために
java.util.Collectionにstream()メソッドなどが追加された。

ただこれを行ってしまうと、
下位互換が大きな問題になる。

標準ライブラリ内の変更なのだからがんばればよい気もするが
Collectionインタフェースではそうはいかない。

Collectionインタフェースは
JDK1.2から存在しているインタフェースで数多く利用されている。
独自の実装が作られていることも多いインタフェースの一つである。

これを変更するということは
それらが軒並みコンパイルエラーになるので、
JavaSE8に変更しようなんて気になれなくなってしまう。

よって、Collectionインタフェースにメソッドを追加するなんて基本的には無理。
実際、JavaSE7まで変更は行われていない。

そこでdefaultを導入することになる。

次のように「default」というキーワードをつけると、
インタフェースに実装が書けるようになる。

public interface Collection<E> extends Iterable<E> {
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
}

これによりCollectionの実装クラスがメソッドを追加されても、
defaultに実装が書かれているのでコンパイルエラーにはならない。

defaultメソッドの実装が気に入らない場合は、
オーバーライドするということになる。

これにより、下位互換の問題がばっちり解決される。


ところで、インタフェースに実装が書けるようになり
インタフェースは複数implements可能なのだから
多重継承できそうな気がするが、さすがにできない。

そこは多重継承が目的ではなく、
下位互換を守るためなのでってことだろう。

処理を極端に簡素に書くためのラムダ式

ストリームAPIとdefaultによって
内部イテレータを実現する仕組みができた。

残りは処理を渡す方法になる。
ただ、Javaでは処理だけを渡す方法はない。

よってGUIなどの例をとると
無名インナークラスというやり方がある。

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World");
    }
};

ただGUIの経験がある方は感じたことがあると思うが、
ごちゃごちゃする感が否めない。

今まではGUIの人にガマンしてもらってきたが、
イテレーションとなるとそうはいかない。

あらゆる人が不快に思っては困るので、
ラムダ式の導入ということになる。

ラムダ式を慣れてない(知らない)人にとっては、
ラムダ式はとても奇妙に見える。

ただあまり恐れることはなくて、
ラムダ計算を本格的にするということではないのだから、
とりあえずは単に処理を簡素に書く方法(表現方法)というとらえ方でいい。

ラムダ式自体は、Javaなんかより遥かに昔から存在する。
関数を書くときは、最低限このぐらいの記述が必須に思える。

int plus(int x,int y){
  return x + y;
}

しかしよく考えるとこれでも冗長で、
「(」「)」括弧といくつかの記号だけで十分表現することができる。

実際Lispのラムダ式では、こんな風に表現できる。

(defun plus (x y) (+ x y))
(plus 1 2)

Javaではこんなかんじになります。

(x,y)->{x+y}

実際には推論がパワーを発揮するので
もっといろいろな書き方があったりする。

ただ肝心なのは、処理を簡素に書くためってことで
一番上のコードより、余計なものがなくなって相当スッキリになった。

まずはfor文→ストリームAPI

ここまでで説明したように並列処理を簡単に行うために、
ストリームAPIやラムダ式が導入されたので、
当然そこらへんがポイントになる。

ただfor文の処理について、全部並列化できるかといわれれば
そんなこともない。

順序が重要な処理は当然あるので、
人によっては並列化できるところは意外と少ないかもしれない。

また、いくらストリームAPIが並列部分を受け持つといっても、
最低限の並列処理に関する知識と経験は持っておきたい。

最初のうちは、処理スピードの恩恵よりも、
並列処理特有のバグにはまることの方が多いかも知れない。

そこでお勧めしたいのは、
for文を一切使わずにストリームAPIでコードを書くことから始めること。

ストリームAPIでは
stream()で順次実行、parallel()で並列実行の
ストリームが形成される。

だから、今は並列化できないところでも
とりあえずはstream()で実装しておく。
そして、必要になったり可能であると判断できたときに
parallelに書き換えるというスタンスがいい。

基本的に今までのfor文は、
全てストリームAPIで置き換えられる。

ストリームAPIを利用していれば、
自然とラムダ式も使いたくなる。

for文よりストリームAPIの方が、
メソッド名が特定されるので
意図が伝わりやすい。

というわけで、JavaSE8が利用可能な人は
こういったところからスタートしてみてはいかがだろうか。

PR