読者です 読者をやめる 読者になる 読者になる

Java8新機能 ラムダ式とデフォルトメソッドの導入理由

前々回に次回予告した内容ですw
ラムダ式とデフォルトメソッドがどう関係あるのかということですが、まずはラムダ式が追加された経緯から読んでみます。

なぜJavaにラムダ式が追加されたのか

Why are lambda expressions being added to Java?

ラムダ式(とクロージャ)は最近の様々な言語で人気が出ています。これには様々な理由がありますが、Javaにとって最も差し迫った理由は、コレクションの処理を複数のスレッドで分散処理することです。 現在、ListやSetはCollectionから取得されたIteratorを使ってその要素が一つずつ順番に処理されるのが普通です。 もしそれぞれの要素を並列に処理しようとした場合、その責任を負うのはCollectionではなく、プログラマがそういったコードを書かなくてはいけません。

Java8では、様々な方法で要素を処理するためのメソッドをCollectionに持たせ、それを利用するようにすることが目的です。 (例えば、とてもシンプルなforEachメソッドを使用することで、すべての要素に対して処理を行えます。) 利点としては、Collectionが自身の要素に対する反復処理を管理でき、並列処理を行う責任を、プログラマからライブラリに移管できます。

しかしそのためには、その機能を提供するためのシンプルなメソッドをCollectionに追加する必要があります。 現在は、適切なInterfaceを実装した無名クラスを利用するのが標準的な方法です。しかし、無名クラスを使用する方法は、実用面から見て非常に不恰好です。

例えば、CollectionのforEachメソッドが、Consumer Interfaceを引数にとり、各要素のacceptメソッドを呼び出す場合、

      interface Consumer<T> { void accept(T t); }

forEachを使用してListに含まれるjava.awt.Pointのxとyの値を入れ替えるとします。 Consumerを実装した無名クラスを使用する場合、このように入れ替えのためのメソッドを渡します。

    pointList.forEach(new Consumer() {
        public void accept(Point p) {
            p.move(p.y, p.x);
        }
    });

しかし、ラムダ式を使用すると、同様のことがはるかに簡潔に実現できます。

    pointList.forEach(p -> p.move(p.y, p.x));

つまり、反復処理を並列に行うための簡単な方法として、Collection InterfaceにforEachが追加されました。
しかし、そのままだと無名クラスを使用した面倒な書き方をしなくてはいけないので、簡潔に書けるようにラムダ式が追加された。
ということでしょうか。

そもそも並列処理が重要な理由としてマルチコアプロセッサの登場があります。並列処理をさせることで、その能力をフルに活かしたいということでしょう。

ちなみにこのサンプルコードですが、並列処理をさせるためには以前の記事でも書きましたが、parallelStreamを使用して

    pointList.parallelStream().forEach(p -> p.move(p.y, p.x)); 

とする必要があります。

なぜJavaにデフォルトメソッドが追加されたのか

What are default methods?

"Why are lambda expressions being added to Java?"のページでは、各要素を処理する例として、CollectionのforEachの例を使用しています。

    pointList.forEach(p -> p.move(p.y, p.x));

しかし、JavaのCollectionフレームワークは15年前に、こういった機能を考慮せずにデザインされたものです。この機能のために必要なforEachやその他のメソッドが含まれていませんでした。 Collectionクラスにメソッドを追加するということは、高いインターフェースベースのフレームワークを破壊します。 Collectionクラスは、それに依存しているコードの決まり事を定義するInterfaceです。(同様に、ユーティリティクラスのCollectionsにstaticなメソッドを追加することは解決策になりません。) つまり、Interfaceに新しいメソッドを追加した場合、そのコレクションにも反映させる必要があります。 しかし、現在Interfaceにメソッドを追加した場合、それを実装している既存のクラスを変更しなくてはいけなくなります。

これがデフォルトメソッド(virtual extension methodsやdefender methodsとも呼ばれます)を導入した理由です。 Interfaceが既存の実装クラスに影響を与えることなく進化することを可能とするのが目的です。 そのためには、Interfaceの目的を大きく変える必要があります。 以前はJavaDocで動作が定義された抽象メソッドを宣言できるだけでした。一方、現在はデフォルト実装を持った具体的なメソッドを宣言することができます。 例えば、仮にIteratorに要素を1つスキップするメソッドを追加する場合

    interface Iterator {
        // existing method declarations
        default void skip() {
            if (hasNext()) next();
        }
    }

もしIteratorがこのように拡張された場合、すべての実装クラスは自動的にskipメソッドを持ち、抽象メソッドと同様に呼び出すことができます。 skipメソッドが呼び出された場合、実装クラスでこのメソッドが定義されていなければ、Interfaceの処理が呼び出されます。 その代わりに、(デフォルトメソッドは仮なので)Iteratorを実装したクラスはデフォルトメソッドをオーバーライドすることができます。

つまり、ラムダ式(というか並列処理)のためにCollection InterfaceにforEachとかを追加したけど、そのままだとCollectionを実装している各クラスでその処理を実装する必要がでてくる。 そうなると、下位互換もできなくなってしまうため、その解決方法としてデフォルトメソッドが追加されたというわけですね。

Interfaceを進化させるためという理由はありますが、なんというか、将来に向けたビジョンを持ってというよりは、必要に迫られて導入された感じがしてちょっと悲しいですねw