はじめに
2013年に予定されるているJDK 8のリリース向けて、OracleはJDK 8に含まれる新しいアイディアをかなり具体化しています。Simon Ritter氏は今年の始めに開催されたQCon LondonでJDK 8に含まれる新しい機能の概要を紹介しました。JDK 8にはモジュラリティ(Project Jigsaw)、JRockit/Hotspot、型に対するアノテーション、Project Lambdaが含まれます。
言語の観点から見れば、最も重要なのはProject Lambdaでしょう。Project Lambdaはラムダ式と仮想拡張メソッドとサポートし、並列コレクションによってマルチコアプラットフォームのサポートを強化します。
これらの新しい機能の多くはJVM上で動く多くの言語で既にサポートされています。Scalaもそのひとつです。さらに、Java 8で採用された手法の多くはScalaに驚くほどにています。結果、Scalaに親しめば、Java 8でのプログラミングがどのようなものがわかります。
この記事ではJava 8の新しい機能を、Scalaと比較しながら紹介します。ラムダ式、高階関数、並列コレクション、想拡張メソッドを紹介します。さらにJava 8に導入された、関数型言語のような新しいパラダイムを考察します。
この記事を読めば、Java 8に導入された新しい概念がどのようなものかわかるでしょう。Scalaにはすでに導入されている機能ですが、ただの飾りではなく、パラダイムシフトを起こす可能性のあるのがわかるでしょう。ソフトウエア開発に大きな可能性をもたらすでしょう。
ラムダ式/関数
Java 8はついにラムダ式を導入します。ラムダ式自体は2009年からProject Lambdaで利用することができました。当時はラムダ式はJava Closuresと言われていました。サンプルコードを紹介する前に、ラムダ式がJavaプログラマにとってなぜ歓迎すべき機能なのかを説明しましょう。
なぜラムダ式を使うのか
ラムダ式の一般的な使い方はGUI開発においてです。一般的に、GUIプログラミングではイベントと結びつく振る舞いを解決する必要があります。例えば、ユーザがボタンを押したとき(イベントが発生したとき), プログラミングは何らかの処理を実行する必要があります。例えば、Swingでは、次のコードで示すようにActionListeners
を使います。
class ButtonHandler implements ActionListener { public void actionPerformed(ActionEvent e) { //do something } } class UIBuilder { public UIBuilder() { button.addActionListener(new ButtonHandler()); } }
この例ではクラスButtonHandler
がコールバックの受取手として設定されています。ButtonHandler
はひとつのメソッドしか保持しません。actionPerformed
です。このメソッドはActionListener
インターフェイスで定義されています。この例は匿名クラスを使うことでコードをシンプルにできます。
class UIBuilder { public UIBuilder() { button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { //do something } } } }
この方がいくらかきれいです。しかし、コードをより詳しく見ると、ひとつのメソッドを呼ぶためだけにインスタンスを作っていることがわかります。この問題はラムダ式を導入することで解決することができます。
関数としてラムダ式
ラムダ式は関数リテラルで、パラメータと関数の本体を定義します。Java 8のラムダ式の構文はまだ検討中ですが、下記のようになるでしょう。
(type parameter) -> function_body
具体的には次のようになります。
(String s1, String s2) -> s1.length() - s2.length();
このラムダ式は2つの文字列長の差を計算します。後述するように引数に型定義がなかったり、{ and }を使って文をまとめることで複数行定義をサポートするなど構文を拡張しています。
Collections.sort()
メソッドはラムダ式の理想的な使い方です。このメソッドを使えばStrings
のコレクションの要素を文字列の長さでソートすることができます。
Listlist = Arrays.asList("looooong", "short", "tiny" ); Collections.sort(list, (String s1, String s2) -> s1.length() - s2.length()); > "tiny", "short", "looooong".
現在のJavaのバージョンを使った場合のようにsort
メソッドにComparator
の実装を与える代わりに、ラムダ式を与えれば同じ結果を得られます。
クロージャとしてのラムダ式
ラムダ式には興味深い特徴があります。ひとつはクロージャです。クロージャを使うと関数は現在の構文スコープの外の変数にアクセスすることができます。
String outer = "Java 8" (String s1) -> s1.length() - outer.length()
この例ではラムダ式はString outer
にアクセスしていますが、この変数はスコープの外に定義されています。インラインで処理を実施する場合、このクロージャはとても便利です。
ラムダ式用の型推論
型推論はJava 7で導入されましたが、ラムダ式にも適用されます。簡単に言えば、型推論を使うことでプログラマは、コンパイラが型を推測できる箇所ならどこでも型の宣言を省略できます。
ソート処理のラムダ式で型推論を使うと次のようになるでしょう。
Listlist = Arrays.asList(...); Collections.sort(list, (s1, s2) -> s1.length() - s2.length());
パラメータs1
とs2
の型の宣言が省略されています。コンパイラがリストがStrings
のコレクションであり、ラムダ式を比較処理に使う場合2つのString
型のパラメータが必要だということを知っているから省略できるのです。明示的に型を宣言する必要はありません。もちろん、明示的に宣言することもできます。
型推論の利点は定型的なコードを削減できることです。コンパイラが型を推論してくれるのなら、どうしてわざわざ宣言する必要があるのでしょうか。
ハロー、ラムダ式、さようなら匿名内部クラス
さて、最初に見たコールバックの実装をラムダ式と型推論を使うとどのようにシンプルになるのか見てみましょう。
class UIBuilder { public UIBuilder() { button.addActionListener(e -> //process ActionEvent e) } }
コールバックメソッドを保持するクラスを定義する代わりに、ラムダ式を直接addActionListener
メソッドに渡します。定型的なコードを削除できる上、読みやすくなり、イベントをハンドルするという関心事だけをはっきりと表現できます。
ラムダ式の利点をさらに詳述する前に、Scalaのラムダ式を見てみましょう。
Scalaのラムダ式
関数型プログラミングでは関数は基本的な構成要素のひとつです。Scalaはオブジェクト指向と関数型プログラミングの両方の特徴を合わせています。Scalaではラムダ式は‘関数’または‘関数リテラル’と呼ばれる基本的な構成要素です。ラムダ式はvals
またはvars
に割り当てることができます。また、他の関数に引数として渡すこともできますし、新しい関数を作るために合成することもできます。
Scalaでは関数リテラルは下記のように記述されます。
(argument) => //function body
例えば、上でJavaのラムダ式でふたつの文字列の長さの差を計算しましたが、Scalaで書くと次のようになります。
(s1: String, s2: String) => s1.length - s2.length
Scalaでは、関数リテラルはクロージャでもあります。構文スコープの外で定義した変数にアクセスできます。
val outer = 10 val myFuncLiteral = (y: Int) => y * outer val result = myFuncLiteral(2) > 20
この例の結果は20です。この例ではmyFuncLiteral
という変数に関数リテラルを割り当てています。
Java 8のラムダ式とScalaの関数は構文的にも意味的にもとても似ています。意味的には両者は全く同じです。構文的には矢印のシンボルが違うだけです(Java8: -> Scala: =>)。簡易表記も違いますがここでは触れません。
再利用可能な構成要素としての高階関数
関数リテラルの大きな利点はString
やObject
など他のリテラルと同じように、様々な処理に引き回せるということです。この利点には様々な用途があり、コンパクトで再利用可能なコードを書くことができます。
高階関数
関数リテラルをメソッドに渡す場合、メソッドを引数に取るメソッドが必要です。このようなメソッドを高階関数と言います。前述のSwingのコードで見たaddActionListener
メソッドはまさに高階関数です。もちろん、独自の高階関数を定義することもできます。高階関数は便利です。次の例を見てください。
def measure[T](func: => T):T = { val start = System.nanoTime() val result = func val elapsed = System.nanoTime() - start println("The execution of this call took: %s ns".format(elapsed)) result }
measure
メソッドはfunc
という関数リテラルを実行するのに費やした時間を計測します。func
のシグネチャは、引数を取らず、ジェネリック型Tの結果を返します。Scalaの関数は実際に引数を取る場合も引数を宣言する必要はありません。
measureメソッドには関数リテラル(メソッド)を渡すことができます。
def myCallback = { Thread.sleep(1000) "I just took a powernap" } val result = measure(myCallback); > The execution of this call took: 1002449000 ns
概念的な視点から考えると、この方法は実際の処理からメソッド呼び出しの時間を計測するという関心事を分離したと言えます。2つの再利用可能なコード(時間計測処理とコールバック)は疎結合になります。
高階関数を使った再利用
2つの再利用可能なコードが強く結合している例を見てみましょう。
def doWithContact(fileName:String, handle:Contact => Unit):Unit = { try{ val contactStr = io.Source.fromFile(fileName).mkString val contact = AContactParser.parse(contactStr) handle(contact) } catch { case e: IOException => println("couldn't load contact file: " + e) case e: ParseException => println("couldn't parse contact file: " + e) } }
doWithContact
メソッドはファイルからvCardのような連絡情報を読み取り、連絡情報ドメインオプジェクトに変換します。このドメインオブジェクトは関数リテラルのコールバックhandle
に渡されます。doWithContact
メソッドとhandleの戻り値はUnit
型です。これはJavaのvoid
メソッドと同じです。
doWithContact
メソッドに渡すことができる様々なコールバックを定義することができます。
val storeCallback = (c:Contact) => ContactDao.save(c) val sendCallback = (c:Contact) => { val msgBody = AConverter.convert(c) RestService.send(msgBody) } val combinedCallback = (c:Contact) => { storeCallback(c) sendCallback(c) } doWithContact("custerX.vcf", storeCallback) doWithContact("custerY.vcf", sendCallback) doWithContact("custerZ.vcf", combinedCallback)
コールバックはインラインで渡すこともできます。
doWithContact("custerW.vcf", (c:Contact) => ContactDao.save(c))
Java 8の高階関数
現在の構文のドラフトを見る限り、Java 8の高階関数もとても似ています。
public interface Block{
void apply(T t);
} public void doWithContact(String fileName, Blockblock) {
try{
String contactStr = FileUtils.readFileToString(new File(fileName));
Contact contact = AContactParser.parse(contactStr);
block.apply(contact);
} catch(IOException e) {
System.out.println("couldn't load contact file: " + e.getMessage());
} catch(ParseException p) {
System.out.println("couldn't parse contact file: " + p.getMessage());
}
} //usage
doWithContact("custerX.vcf", c -> ContactDao.save(c))
高階関数の利点
ご覧の通り、関数を使うことでドメインオブジェクトの作成と処理をきれいに分離できます。こうすることで連絡情報ドメインオブジェクトの新しい処理方法をドメインオブジェクトの作成処理と分離したまま追加することができます。
高階関数の利点はコードをDRY(Don’t Repeat Yourself)に保つことができ、プログラマが細かい粒度のコードを適切に再利用できるということです。
コレクションと高階関数
高階関数はコレクションを処理するのに極めて効率的な方法を提供します。ほぼすべてのプログラムがコレクションを使うので、コレクション処理が効率的になるのは大きな利点です。
コレクションのフィルタリング
コレクションを使った一般的な例を見ましょう。コレクションの各要素に処理を適用したいとします。例えば、写真オブジェクトのリストがあり、あるサイズの写真オブジェクトだけ抽出したいとします。
Listinput = Arrays.asList(...); List output = new ArrayList(); for (Photo c : input){ if(c.getSizeInKb() < 10) { output.add(c); } }
このコードには定型的なコードがたくさん含まれています。処理結果のコレクションを作成したり、リストに新しい要素を追加したりするコードです。これ以外の書き方としては、Functionクラスを使う方法があります。Functionクラスは関数の振る舞いを抽象化します。
interface Predicate{ boolean apply(T obj); }
Guavaを使うとこの結果は次のように書けます。
final Collectioninput = Arrays.asList(...); final Collection output = Collections2.transform(input, new Predicate (){ @Override public boolean apply(final Photo input){ return input.getSizeInKb() > 10; } });
この書き方は定型的なすが、まだ汚いです。このコードをScalaかJava 8で書き直すとラムダ式の強力さとエレガントさがわかるでしょう。
Scala:
val photos = List(...) val output = photos.filter(p => p.sizeKb < 10)
Java 8:
Listphotos = Arrays.asList(...) List output = photos.filter(p -> p.getSizeInKb() < 10)
両方ともエレガントで簡潔な実装です。両方とも型推論が使えるということに注意してください。関数の引数p
の型Photo
は明示的に宣言されていません。これまで見てきたように、Scalaでは型推論は標準的な機能です。
Scalaの関数連鎖
これまで、少なくとも6行のコードを削減し可読性を改善しました。面白いのは複数の高階関数を連鎖させる方法です。これを説明するためにScalaでPhoto
クラスを作り、属性を追加してみましょう。
case class Photo(name:String, sizeKb:Int, rates:List[Int])
上のコードは単にPhoto
クラスを作成して、名前、sizeKb
、 rates
の3つインスタンス変数を宣言しているだけです。変数ratesにはこの写真のユーザの評価を持ちます。取り得る値は1から10です。次のようにPhoto
クラスを作ります。
val p1 = Photo("matterhorn.png", 344, List(9,8,8,6,9)) val p2 = ... val photos = List(p1, p2, p3, ...)
Photo
クラスのインスタンスのリストに対して、複数の高階関数を連鎖させることで様々な問い合わせ処理を定義できます。例えば、10MB以上の写真の名前を抽出する処理です。まず問題になるのはPhotoクラスのリストをファイル名のリストに変換する方法です。これを実現するために高階関数を使います。使うのはmapと呼ばれる最も強力な高階関数のひとつです。
val names = photos.map(p => p.name)
map
メソッドはコレクションの各要素をmapメソッドに渡された関数に定義されている型に変換します。この例ではPhoto
オブジェクトを受け取り、画像の名前であるString
を返す関数が定義されています。
map
をfilter
メソッドと連鎖させることで目的の処理を実現できます。
val fatPhotos = photos.filter(p => p.sizeKb > 10000) .map(p => p.name)
NullPointerExceptions
の心配はありません。各メソッド(filter、map
など)は常にコレクションを返します。空のコレクションの場合はありますが、null
にはなりません。Photoのコレクションが始めから空であれば、計算結果も空のコレクションになるでしょう。
関数連鎖は‘関数合成’とも言われます。関数合成を使うことで、コレクションのAPIから問題を解決するための処理を探すことができます。
より先進的な例を見てみましょう。
問題: "平均評価が6よりも高い写真の名前を取得せよ。ただし、評価の合計値順にソートすること。"
val avg = (l:List[Int]) => l.sum / l.size val minAvgRating = 6 val result = photos.filter(p => avg(p.ratings) >= minAvgRating) .sortBy(p => p.ratings.size) .map(p => p.name)
この処理を実現するには、sortBy
メソッドを使います。このメソッドは入力としてコレクションの要素の型を受け取り(この場合はPhoto
)、Ordered
型のオブジェクト(この場合はInt
)を返します。List
には平均を算出するメソッドはありません。したがって、与えられたInt
のList
の平均を算出する関数リテラルavg
を定義しています。この関数リテラルは匿名関数としてfilter
メソッドに渡されます。
Java 8での関数連鎖
Java 8のコレクションクラスがどのような高階関数を提供するのか現時点では明確ではありません。Filter
と map
がサポートされる可能性は高いです。最初に示した関数連鎖の例をJava 8で書くと下記のようになるでしょう。
Listphotos = Arrays.asList(...) List output = photos.filter(p -> p.getSizeInKb() > 10000) .map(p -> p.name)
特筆すべきは、Scalaの場合と構文的な違いがほとんどないことです。
コレクションと組み合わせた高階関数はとても強力です。簡潔で読みやすいだけでなく、たくさんの定型的なコードを省けます。それゆえ、少ないテストで済み、バグも少なくなります。
並列コレクション
これまで、高階関数を使ったコレクションの処理の最も強力な利点を紹介していませんでした。簡潔さと読みやすさに加えて、高階関数はとても重要な抽象レイヤを提供します。前述した例はすべてループがありません。フィルタ処理やマップ処理、ソート処理のために一度もコレクションに対して反復処理をしていません。反復処理は隠されています。別の言い方をすれば抽象化されているのです。
この抽象化レイヤはマルチコア環境を利用するための鍵です。というのは、抽象レイヤの下にあるループの実装はどのように反復処理をするかを選択できるからです。つまり、反復処理は順次実行されるだけでなく、並列で実行することもできるのです。マルチコア環境において、並列処理はもはや「可能であれば実装したい」という段階ではありません。今日のプログラミング言語は並列環境の要求に答える必要があります。
理論的には、並列処理のコードを自前で書くことはできます。しかし、実際には自前で書くのは賢いやり方ではありません。第一に、しっかりとした並列処理を書くのはとても難しいです。とりわけ、状態を共有し、処理の中間結果を最終的に統合する場合は大変です。第二に、多くの異なる並列処理実装が生まれるのは望ましくありません。Java7にはFork/Joinがありますが、データの分解処理と統合処理はクライアント側で書かなければなりません。ここには私たちが求める抽象レイヤはありません。第三に、関数型プログラミングに既に答えがあるのに、これ以上悩む必要はありません。
つまり、賢い人々に並列反復処理のコードを書いてもらい、高階関数を使ってそのコードの使い方を抽象化すればいいのです。
Scalaの並列コレクション
Scalaの並列処理の簡単な例を見ましょう。
def heavyComputation = "abcdefghijk".permutations.size (0 to 10).par.foreach(i => heavyComputation)
まず、heavyComputation
メソッドを定義します。このメソッドは重い処理を実行します。クアッドコアのラップトップではこの処理は実行するのに4秒かかります。そして、範囲のコレクション(0 to 10
)をインスタンス化し、par
メソッドを実行します。par
メソッドは並列処理の実装を返却します。この並列処理の実装は並列でない処理と全く同じインターフェイスを持っています。ほとんどのScalaのコレクション型はpar
メソッドを持ちます。たったこれだけで並列処理が実現できます。
クアッドコアのマシンでどのくらい性能が改善するか確認して見ましょう。先に取り上げたmeasureメソッドを使い、性能調査をそます。
//single execution measure(heavyComputation) > The execution of this call took: 4.6 s //sequential execution measure((1 to 10).foreach(i => heavyComputation)) > The execution of this call took: 46 s //parallel execution measure((1 to 10).par.foreach(i => heavyComputation)) > The execution of this call took: 19 s
驚くべきことは、並列処理の場合、4つのコアを使っているにも関わらず、たった2.5倍だけしか速くならないということです。これは、並列処理をすることで新たなオーバーヘッド、つまり、スレッドを起動する処理と中間の結果を統合する処理にかかるオーバヘッドが生まれるからです。したがって、どのような処理でも並列コレクションを使うというのは得策ではありません。重い処理にのみ適用するのがいいでしょう。
Java 8の並列コレクション
現在提案されているJava 8の並列コレクションのインターフェイスはほとんどScalaと同じです。
Array.asList(1,2,3,4,5,6,7,8,9.0).parallel().foreach(int i -> heavyComputation())
パラダイムはScalaと全く同じです。並列コレクションを作成するメソッドの名前がpar
ではなくparallel()
であるのがわずかな違いです。
すべて一度に: より大きな例
高階関数/ラムダ式と並列コレクションの組み合わせを、より大きな例を使って簡単にまとめてみましょう。この例では、Scalaで上述した概念を組み合わせて使っています。
この例では、美しい壁紙を提供するサイトから、画像のURLを抽出し画像を並列処理でダウンロードします。Scalaの標準ライブラリ以外に2つのライブラリを使いました。 HTTP通信のためのライブラリDispatchとApacheのFileUtilsです。この記事では紹介しないScalaの特徴が出てきますが、何をしようとしているかは理解できると思います。
import java.io.File import java.net.URL import org.apache.commons.io.FileUtils.copyURLToFile import dispatch._ import dispatch.tagsoup.TagSoupHttp._ import Thread._ object PhotoScraper { def main(args: Array[String]) { val url = "http://www.boschfoto.nl/html/Wallpapers/wallpapers1.html" scrapeWallpapers(url, "/tmp/") } def scrapeWallpapers(fromPage: String, toDir: String) = { val imgURLs = fetchWallpaperImgURLsOfPage(fromPage) imgURLs.par.foreach(url => copyToDir(url, toDir)) } private def fetchWallpaperImgURLsOfPage(pageUrl: String): Seq[URL] = { val xhtml = Http(url(pageUrl) as_tagsouped) val imgHrefs = xhtml \\ "a" \\ "@href" imgHrefs.map(node => node.text) .filter(href => href.endsWith("1025.jpg")) .map(href => new URL(href)) } private def copyToDir(url: URL, dir: String) = { println("%s copy %s to %s" format (currentThread.getName, url, dir)) copyURLToFile(url, new File(toDir, url.getFile.split("/").last)) }
コードの説明
scrapeWallpapers
メソッドは処理の流れを制御し、htmlから画像を取り出してダウンロードします。
fetchWallpaperImgURLsOfPage
メソッドはすべての壁紙の画像URLをhtmlから取り出します。
Http
オブジェクトはDispatchライブラリのクラスです。このクラスはApacheのhttpclientライブラリ関連のDSLを提供します。as_tagsouped
メソッドはhtmlをxmlに変換します。xmlはScalaに組み込まれているデータ型です。
val xhtml = Http(url(pageUrl) as_tagsouped)
xhtmlからダウンロードしたい画像に関連するhrefを探します。
val imgHrefs = xhtml \\ "a" \\ "@href"
ScalaはXMLをネイティブでサポートしますので、xpathライクな式 \\ を使ってノードを選択できます。すべてのhrefを検索したら、画像のURLを抽出し、hrefをURLオブジェクトに変換します。この処理に使うのがmapやfilter
のようなScalaのコレクションAPIの高階関数の連鎖です。結果は画像URLのリストになります。
imgHrefs.map(node => node.text) .filter(href => href.endsWith("1025.jpg")) .map(href => new URL(href))
次に画像を並列処理でダウンロードします。並列でダウンロードするために画像の名前のリストを並列コレクションに変換します。 そうすることで、foreach
メソッドは複数のスレッドを立ち上げてコレクションに同時にループ処理を適用します。各スレッドは最終的にはcopyToDir
メソッドを呼びます。
imgURLs.par.foreach(url => copyToDir(url, toDir))
copyToDir
メソッドはApache CommonのライブラリFileUtilsを使います。FileUtil
クラスの静的メソッドcopyURLToFile
を静的にインポートしているので直接呼び出すことができます。処理を明確にするために処理を実行したスレッドの名前を表示しています。実際に並列処理を実行してみると複数のスレッドが忙しく処理を実行しているのがわかります。
private def copyToDir(url: URL, dir: String) = { println("%s copy %s to %s" format (currentThread.getName, url, dir)) copyURLToFile(url, new File(toDir, url.getFile.split("/").last)) }
このメソッドでわかるのは、ScalaはJavaの既存のライブラリを利用できるということです。
Scalaの関数的な特徴とコレクションに対する高階関数の利点、並列処理を使うことで、データを解析し、入出力を扱い、データを変換する並列処理をわずか数行で実行できるのです。
仮想拡張メソッド/trait
Javaの仮想拡張メソッドはScalaの‘trait’と似ています。ではtraitと何でしょう。Scalaのtraitはインターフェイスを提供しますが、実装も提供することができます。この構造は大きな可能性を生み出します。traitを使ってクラスを構成してみるとよくわかるでしょう。
Java 8と同じように、Scalaは多重継承をサポートしていません。JavaもScalaもサブクラスが継承できるスパークラスはひとつだけです。しかし、traitを使えば違います。ひとつのクラスは複数のtraitを"mix in"することができます。興味深いのはtraitを使うとクラスはそのtraitの型やメソッドや状態をすべて利用できるということです。このような特性からtraitはmixinとも呼ばれます。クラスに新しい振る舞いをや状態を混ぜる(mix in)からです。
しかし、まだ疑問があるでしょう。traitがある種の多重継承をサポートするなら、悪名高い"ダイヤモンド問題"に苦しむことになるのではないか。答えはもちろんノーです。Scalaは多重継承のヒエラルキーの中でいつ何が実行されるのか明確に決まっています。この規則は利用しているtraitの数とは関係ありません。この規則のおかげで、複雑な問題に邪魔されずに多重継承の利点を享受できるのです。
traitが使える場合
次のコードはJava開発者には馴染みがあるでしょう。
class Photo { final static Logger LOG = LoggerFactory.getLogger(Photo.class); public void save() { if(LOG.isDebugEnabled()) { LOG.debug("You saved me." ); } //some more useful code here ... } }
設計の観点から見ればロギングは横断的な関心事です。しかし、日々のJava開発の中ではそのような設計の観点を踏まえた実装はおほとんど見当たりません。各クラスでロガーを宣言しているのが実情です。さらにこの例ではログレベルをチェックするためにisDebugEnabled()
メソッドを使っています。このような書き方はDRY(Don’t Repeat Yourself)に明らかに違反しています。
Javaの場合、プログラマがログレベルのチェックを宣言していることは正当化できません。しかし、クラスに関連した適切なロガーを使う方法もありません。Java開発者はこの書き方に親しんでいます。今やひとつのパターンです。
traitはこのパターンに対して優れた代替案を提供します。traitにロギング機能を持たせ、ログ出力を加えたいクラスにこのtraitをmix-inすることができます。こうすることでクラスは‘ロギング’という横断的関心事にアクセスできます。
traitを使ったロギング
Scalaでは、次のようにLoggable
traitを実装します。
trait Loggable { self => val logger = Slf4jLoggerFactory.getLogger(self.getClass()) def debug[T](msg: => T):Unit = { if (logger.isDebugEnabled()) logger.debug(msg.toString) } }
Scalaは‘trait’
キーワードを使ってtraitを宣言します。traitの本体にはフィールドやメソッドなど抽象クラスで宣言できるものなら何でも含むことができます。この例で興味深いのは、self =>
です。このロガーは、Loggableそのものではなく、Loggable
traitを使っているクラスをロギングします。Scalaではself =>
という構文は自己型(self-type)と呼ばれ、traitがそのtraitを使っているクラスを参照するときに使われます。
debugメソッドのパラメータとして、引数なしの関数msg: => T
を使っていることに注意してください。isDebugEnabled()
でチェックしている理由はデバッグログレベルが有効なときだけ出力するString
を計算するようにするためです。debug
メソッドが単純にString
の引数を受け取るだけだと、デバッグログレベルの有効、無効に関わらず出力する文字列を求めてしまいます。これは望ましくありません。String
を受け取る代わりに引数のない関数msg: => T
を受け取ることで、望み通りの処理になります。関数msgはisDebugEnabled
のチェックに通った場合のみ、出力する文字列を返します。isDebugEnabled
のチェックに通らなかったとき、関数msg
は呼ばれませんので、String
を求めるという不必要な処理は実行されません。
PhotoクラスでLoggable
traitを使いたい場合、extends
を使います。
class Photo extends Loggable { def save():Unit = debug("You saved me"); }
‘extends’
キーワードがあるので、Photo
はLoggable
から派生し、他のクラスは継承できないと思えますが、正しくありません。Scalaの構文ではクラスを拡張とtraitの利用に‘extends’
を使います。traitを複数使いたいのなら、キーワード‘with’
を使い、使いたいtraitを並べます。‘with’
キーワードを使った例は後述します。
Photo
クラスのメソッドsave()
を呼び出して、実際の動作を確かめてみましょう。
new Photo().save() 18:23:50.967 [main] DEBUG Photo - You saved me
クラスに振る舞いを追加する
前段で説明した通り、クラスは複数のtraitを利用できます。ロギングの他に、Photo
クラスにさらに振る舞いを追加することができます。例えば、Photo
をファイルの大きさ順に並べたいとしましょう。幸運なことに、Scalaの場合、たくさんのtraitが最初から使えます。これらのtraitの中にOrdered[T]
traitがあります。Ordered
はJavaのインターフェイスComparable
に似ています。大きな違いはScalaのOrdered
は実装も提供するということです。
class Photo(name:String, sizeKb:Int, rates:List[Int]) extends Loggable with Ordered[Photo]{ def compare(other:Photo) = { debug("comparing " + other + " with " + this) this.sizeKb - other.sizeKb } override def toString = "%s: %dkb".format(name, sizeKb) }
上記の例では、ふたつのtraitが使われています。先に定義したLoggable
traitに加え、Ordered[Photo]
traitが使われています。Ordered[T]
traitを使うにはcompare(type:T)
メソッドの実装が必要です。これもJavaのComparable
に似ています。
compare
メソッドに加えて、Ordered
traitは多くのメソッドを提供します。これらのメソッドはオブジェクトを比較するための様々な方法を提供します。これらのメソッドはすべてcompareメソッドの実装を利用します。
val p1 = new Photo("Matterhorn", 240) val p2 = new Photo("K2", 500) p1 > p2 > false p1 <= p2 > true
Scalaでは> や <= などのシンボルはJavaとは違い特別な予約語ではありません。>, <= などを使ってオブジェクトを比較できるのは、Ordered
traitがこれらのシンボルを使ったメソッドを実装しているからです。
Ordered
traitを実装したクラスはScalaのコレクションでソートできます。Ordered
オブジェクトのコレクションで‘sorted’
が呼ばれると、compare
メソッドで定義された順に並び替えが行われます。
val p1 = new Photo("Matterhorn", 240) val p2 = new Photo("K2", 500) val sortedPhotos = List(p1, p2).sorted > List(K2: 500kb, Matterhorn: 240kb)
traitの利点
上述した例では、traitを使うことでモジュール化の方式に従って汎用的な機能を分離することができることを示しました。分離した機能はその機能を必要とするクラスに追加することができます。Photo
クラスにロギングの機能が必要なら、Loggable
traitを使えば実現可能ですし、並び替えが必要ならOrdered
traitを使えばいいのです。これのtraitは他のクラスで再利用可能です。
traitは言語に組み込まれた機能だけでモジュールを作成し、DRY(Don’t Repeat Yourself)なコードを書ける強力な仕組みです。
なぜ仮想拡張メソッドを使うのか
Java 8の仕様のドラフトには仮想拡張メソッドの定義が含まれています。仮想拡張メソッドは既存のインターフェイスの既存のメソッドや新しいメソッドにデフォルトの実装を与えます。ではなぜ必要なのでしょうか。
多くのインターフェイスが高階関数のかたちでラムダ式をサポートととても便利になります。例えば、java.util.Collection
インターフェイスを考えてみましょう。java.util.Collection
インターフェイスがforEach(lambdaExpr)
メソッドを提供すればとても便利でしょう。しかし、このメソッドがデフォルトの実装なしに追加されたら、このインターフェイスを実装しているクラスはデフォルトの実装を書かなければなりません。この方法では互換性に大きな問題が発生するのは明らかです。
これがJDKのチームが仮想拡張メソッドを導入しようとしている理由です。仮想拡張メソッドがあれば、例えばデフォルトの実装を含んだforEach
メソッドをjava.util.Collection
に追加することができます。この場合、java.util.Collection
インターフェイスを実装しているすべてのクラスは自動的にforEach
メソッドとデフォルトの実装を継承します。こうすることで、インターフェイスを実装しているクラスに影響を与えずにAPIを成長させることができます。まさにこれが仮想拡張メソッドの目的です。デフォルトの実装が実装クラスに合わないものだったら、オーバーライドすればいいのです。
仮想拡張メソッド vs traits
仮想拡張メソッドの主な使い方はAPIの拡張です。しかし、仮想拡張メソッドは振る舞いの多重継承を提供します。一方、Scalaのtraitは振る舞いだけでなく状態の多重継承も提供します。状態と振る舞いに加え、traitは実装クラスを参照できます。これはLoggable
の‘self‘
フィールドの例で見た通りです。
使い方の観点から考えると、traitは仮想拡張メソッドよりも多くの機能を提供します。しかし、両者の目的は異なります。Scalaでは、traitは常に‘問題なく’多重継承を提供できるモジュールを提供するのが目的です。一方、仮想拡張メソッドの第一の目的はAPIの進化を可能にすることであり、‘振る舞いの多重継承’は二の次です。
Java 8のLoggable traitとOrdered trait
仮想拡張メソッドで実現できることを確かめるために、Java 8でOrdered
traitとLoggable
traitを実装してみましょう。
Ordered
traitは仮想拡張メソッドで完全に実装できます。状態は関係ないからです。前述の通り、JavaでScalaのOrdered
traitに対応するのはjava.lang.Comparable
です。実装は次のようになるでしょう。
interface Comparable{ public int compare(T that); public boolean gt(T other) default { return compare(other) > 0 } public boolean gte(T other) default { return compare(other) >= 0 } public boolean lt(T other) default { return compare(other) < 0 } public boolean lte(T other) default { return compare(other) <= 0 } }
既存のComparable
インターフェイスに比較のためのメソッドを追加してます(‘より大きい’、‘以上’、‘より小さい’、‘以下’の4つ。Ordered traitではそれぞれ、>、>=、<、<=)。デフォルトの実装はdefault
キーワードで印がついています。すべて既存の抽象メソッドcompareを呼び出しています。結果として、既存のインターフェイスは新しいメソッドで強化されたものの、Comparable
を実装しているクラスはこれらの新しいメソッドを実装する必要はありません。ScalaのOrdered
traitはこの実装にとてもよく似ています。
Photo
クラスがComparable
を実装しているなら、次のようにこれらの新しいメソッドで比較処理を実現できます。
Photo p1 = new Photo("Matterhorn", 240) Photo p1 = new Photo("K2", 500) p1.gt(p2) > false p1.lte(p2) > true
Loggable
traitは仮想拡張メソッドだけでは完全に実装できません。
interface Loggable { final static Logger LOG = LoggerFactory.getLogger(Loggable.class); void debug(String msg) default { if(LOG.isDebugEnabled()) LOG.debug(msg) } void info(String msg) default { if(LOG.isInfoEnabled()) LOG.info(msg) } //etc... }
この例では、Loggable
インターフェイスにdebug、info
などのログ出力メソッドを追加しています。デフォルトではこれらのメソッドの呼び出しはLogger
に移譲されています。この例にないのは実装クラスの参照を得る方法です。この仕組みがないので、Loggable
インターフェイスをロガーとして使う必要があります。このロガーは実装クラスの代わりにLoggable上ですべての文を出力します。この制限があるので、仮想拡張メソッドはこの用途にはあまり向きません。
traitと仮想拡張メソッドは両方とも振る舞いの多重継承を提供しますが、traitは状態の多重継承と実装クラスへの参照を提供します。
結論
Java 8はさまざまな特徴が追加されます。これらの特徴はアプリケーション開発を根本的に変える可能性があります。特に、ラムダ式のような関数型言語の特徴が取り込まれるのはパラダイムシフトだと考えられます。この変化は簡潔でコンパクトで簡単に理解できるコードを記述するための新たな可能性を提供します。
さらにラムダ式は並列処理を有効に活用するためにも重要です。
この記事で紹介したように、これらすべての特徴はすでにScalaで利用できます。試してみたい開発者はJava 8の早期ビルド版を試してみると言いでしょう。あるいは、来るパラダイムシフトに備えてScalaを触ってみるのもいいかもしれません。
Appendix
Java 8の情報は主に下記のプレゼンを参考にしました。
Java 7 and 8: WhereWe'veBeen, WhereWe'reGoing
著者について
Urs Peter氏はXebiaのシニアコンサルタントです。10年間のIT業界の経験の中で氏は開発者からソフトウエアアーキテクト、チームリーダ、スクラムマスタなど多くの役割を果たして来ました。また、JVM上の様々な言語や開発手法、ツールについても理解を深めてきました。氏はヨーロッパで最も早く認定Scalaトレーナーになった1人であり、現在はDutch Scala Enthusiastコミュニティ(DUSE)の議長でもあります。. | |
Sander van den Bergは1999年よりITに従事しています。防衛産業のいくつかの会社でソフトウエア開発者として働き、主にMDA/MDDソリューションの開発に従事しました。2010年、シニアコンサルタントとしてXebiaに参画し、現在は同社でリーンアーキテクチャ手法とScalaの普及に従事しています。氏はアーキテクチャ以外に言語の設計にも興味を持ち、複数の関数型言語のコミュニティが活動しています。氏は難しい問題をエレガントに解決するのが好きです。Clojure、Haskell、F#にも親しんでいます。 |