ラムダ式でCommandパターンで書かれたコードをシンプルにする ~ ラムダ式使いへの第一歩

CodeZine / 2014年12月17日 14時0分

図5:亀シミュレータのクラス図(ラムダ式)

 Java SE 8では、新しい構文要素として「ラムダ式」が導入されました。これにより、これまで用いられたデザインパターンのいくつかは、特に意識しなくても同等のコードが書けるようになります。デザインパターンが解決しようとしていた問題が、ラムダ式によって素直に実装できるからです。本連載では、デザインパターンを使って書かれたコードをラムダ式を使ったシンプルなコードに書き換えながら、ラムダ式の使いどころ・使い方を学んでいきます。

■言語の洗練によってパターンは背景に退く

 まず、デザインパターンについておさらいしましょう。デザインパターンとは、オブジェクト指向言語を用いてプログラムを書くときに、頻発するプログラム設計上の工夫を「パターン」としてまとめたものです。ギャング・オブ・フォー(GoF)と呼ばれる4人の著者(エーリヒ・ガンマ、リチャード・ヘルム、ラルフ・ジョンソン、ジョン・ブリシディース)が執筆した『オブジェクト指向における再利用のためのデザインパターン』(ソフトバンククリエイティブ刊)という書籍では、オブジェクト生成に関するパターン(Factory Methodパターン、Builderパターンなど)、構造に関するパターン(Adapterパターン、Decratorパターンなど)、振る舞いに関するパターン(Template Methodパターン、Strategyパターンなど)に含まれる23のパターンがリストアップされています。

 しかし、少し考えてみると、デザインパターンは、プログラミング言語の表現力不足を克服するための一種の回避策と見ることができます。なぜなら、プログラミング言語に充分な表現力があり、ある種の目的が工夫なしで簡潔に実装できるのであれば、あえて「デザインパターン」として切り出す必要がないはずだからです。

 例えば、かつてのJavaには、組み込みの列挙型がありませんでした。このため、一群の定数を定義する場合にはTypesafe Enumパターンが用いられました[1]。つまり、クラスのコンストラクタをprivateにして、public static finalのフィールドにインスタンスを格納するなど、いくつかの工夫を重ねることで定数を定義したのです。2004年にリリースされたJ2SE 5.0では、enumキーワードによって定義する列挙型が導入されたため、このパターンを用いる必要はなくなりました。

[1] Joshua Bloch, Effective Java, 2nd ed., Addison-Wesley, 2008, Item 30

 さて、Java SE 8では、ひとまとまりの処理を表すオブジェクトを、簡潔な書き方で生成するための構文として ラムダ式 が導入されました。Java SE 7までは、(1)クラスを定義し、(2)メソッドを実装し、(3)インスタンスを生成する、という3ステップを要したプログラムが、Java SE 8では単一のラムダ式で実現できます。このように、ラムダ式によってJava言語が洗練され、表現力が増したことにより、GoFが定義した「振る舞いに関するパターン」のうち次に挙げるものについては、パターンを意識することなくプログラミングできるようになったと考えられます。

Commandパターン Observerパターン Strategyパターン Template MethodパターンとFactoryメソッドの組み合わせ  この連載では、上記のパターンと同様のプログラムをラムダ式を使って実現して(置き換えて)いきます。解説は、Java SE 7までの書き方と比較しながら進めます。

■ラムダ式の概要

 ラムダ式によるデザインパターンの置き換えを説明する前に、ラムダ式についておさらいをしておきましょう。

 ラムダ式とは、「抽象メソッドを1つだけ持つインタフェース」のインスタンスを簡潔に生成する構文です。また、このようなインタフェースのことを 関数的インタフェース と呼びます。

 関数的インタフェースのインスタンスをラムダ式を使わずに生成するには、通常、リスト1の(1)のように関数的インタフェースを実装するクラスを宣言し、(2)のように関数的インタフェースの唯一の抽象メソッドをオーバーライドします。その上で、リスト2の(3)ようにクラスをインスタンス化します。

リスト1:関数的インタフェースのインスタンスをラムダ式を使わずに生成する
// (1) クラスの宣言 class クラス名 implements インタフェース名 { // (2) メソッドのオーバーライド 戻り値型 メソッド名(引数リスト) { 処理 } }
 

リスト2:関数的インタフェースの実装クラスのインスタンス化
// (3) インスタンスの生成 インタフェース名 変数 = new クラス名();
 

 ラムダ式を使うと、(1)、(2)、(3)が1つの式で表せます。リスト1とリスト2の組み合わせは、ラムダ式を使うと、リスト3のように簡潔に表せます。

リスト3:リスト1とリスト2の組み合わせをラムダ式で記述
// (1)、(2)、(3) ラムダ式によるインスタンスの生成 インタフェース名 変数 = (引数リスト) -> { 処理 };
 

 リスト3の中の、(引数リスト) -> { 処理 } の部分がラムダ式です。ラムダ式の中には、インタフェース名も、クラス名も、メソッド名も登場しないことに注意してください。これらはいずれも、次に説明するとおり、コンパイラあるいは実行時のJava VMによって適切に解釈・処理されます。

インタフェース
インタフェースはラムダ式が現れる文脈から推論されます。リスト3では、ラムダ式が代入される変数の型であるインタフェース名が使われます。

クラス
クラスはコンパイラによって静的に自動生成されるか、あるいは実行時のJava VMによって動的に自動生成されます。

メソッド
関数的インタフェースの唯一の抽象メソッドがオーバーライドされます。

 例として、ラムダ式を使って関数的インタフェースのインスタンスを生成するプログラムを、ラムダ式を使わないプログラムと比較してみましょう。リスト4はラムダ式を使わない例です。

リスト4:ラムダ式を使わない例
public class NoLambda { interface StringOp { String apply(String arg); } static class QuoteStringOp implements StringOp { @Override public String apply(String arg) { return String.format("「%s」", arg); } } public static void main(String[] args) { StringOp quoteOp = new QuoteStringOp(); System.out.println(quoteOp.apply("こんにちは世界")); // 出力: 「こんにちは世界」 } }
 StringOp インタフェースの apply メソッドは、文字列を引数に取って文字列を戻すメソッドとして宣言されています。QuoteStringOp クラスは StringOp インタフェースを実装しており、apply メソッドは引数の文字列を鍵括弧でくくって戻すように定義されています。main メソッドでは、QuoteStringOp クラスをインスタンス化して、apply メソッドを呼び出しています。

 StringOp インタフェースの抽象メソッドは apply メソッドだけなので、関数的インタフェースの要件を満たします。このため、リスト5のように、ラムダ式を用いて StringOp インタフェースのインスタンスが生成できます。

リスト5:ラムダ式を用いたStringOpインタフェースのインスタンス生成
public class UsingLambda { @FunctionalInterface interface StringOp { String apply(String arg); } public static void main(String[] args) { StringOp quoteOp = (String arg) -> { return String.format("「%s」", arg); }; System.out.println(quoteOp.apply("こんにちは世界")); // 出力: 「こんにちは世界」 } }
 main メソッドにある quoteOp 変数への代入式の右辺「 (String arg) -> { return String.format("「%s」", arg); } 」がラムダ式です。このラムダ式は、次のように簡潔に書く(略記する)ことも可能です。

引数の型を省略: (arg) -> { return String.format("「%s」", arg); } 引数リストの括弧を省略: arg -> { return String.format("「%s」", arg); } 処理の中括弧と return キーワードを省略: arg -> String.format("「%s」", arg) また、リスト5では、``StringOp インタフェースに @FunctionalInterface```これは、インタフェースが関数的インタフェースであることを明示するアノテーションで、付与することにより、インタフェースがラムダ式によるオブジェクト生成に対応していることが分かりやすくなります。また、関数的インタフェース以外に誤って付与した場合にエラーとして検知する機能もあるため、できる限り付与したほうがよいでしょう。本連載でもこれ以降、ラムダ式を用いるプログラムでは @FunctionalInterface を付与します。

 ここまで、関数的インタフェースやラムダ式を扱うためのルールについて記述してきましたが、使いこなすためには、関数的インタフェースやラムダ式が意味するものの感覚をつかむことも重要です。感覚的な言い方をすると、関数的インタフェースは、ひとまとまりの「処理」や「動作」を表すインタフェースだと考えられます。例えば、リスト4とリスト5の StringOp インタフェースは、文字列を変換して新しい文字列を作るような処理一般を表しているものとみなせます。同様にラムダ式は、具体的な「処理」や「動作」をオブジェクトとして生成するものだと考えられます。例えば、リスト5のラムダ式は、文字列を括弧でくくるという処理をオブジェクトとして生成しているとみなせるのです。



CodeZine

トピックスRSS

ランキング