BT

Java 8 vs Scala: 特徴を比較する

| 作者 Urs Peter フォローする 0 人のフォロワー , Sander van den Berg フォローする 0 人のフォロワー , 翻訳者 徳武 聡 フォローする 1 人のフォロワー 投稿日 2012年7月16日. 推定読書時間: 45 分 |

原文(投稿日:2012/06/29)へのリンク

はじめに

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のコレクションの要素を文字列の長さでソートすることができます。

List  list = 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で導入されましたが、ラムダ式にも適用されます。簡単に言えば、型推論を使うことでプログラマは、コンパイラが型を推測できる箇所ならどこでも型の宣言を省略できます。

ソート処理のラムダ式で型推論を使うと次のようになるでしょう。

List list = Arrays.asList(...); Collections.sort(list, (s1, s2) -> s1.length() - s2.length());

パラメータs1s2の型の宣言が省略されています。コンパイラがリストが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: =>)。簡易表記も違いますがここでは触れません。

再利用可能な構成要素としての高階関数

関数リテラルの大きな利点はStringObjectなど他のリテラルと同じように、様々な処理に引き回せるということです。この利点には様々な用途があり、コンパクトで再利用可能なコードを書くことができます。

高階関数

関数リテラルをメソッドに渡す場合、メソッドを引数に取るメソッドが必要です。このようなメソッドを高階関数と言います。前述の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, Block block) {
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)に保つことができ、プログラマが細かい粒度のコードを適切に再利用できるということです。

コレクションと高階関数

高階関数はコレクションを処理するのに極めて効率的な方法を提供します。ほぼすべてのプログラムがコレクションを使うので、コレクション処理が効率的になるのは大きな利点です。

コレクションのフィルタリング

コレクションを使った一般的な例を見ましょう。コレクションの各要素に処理を適用したいとします。例えば、写真オブジェクトのリストがあり、あるサイズの写真オブジェクトだけ抽出したいとします。

List input = 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 Collection input = 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:

List photos = 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クラスを作成して、名前、sizeKbratesの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を返す関数が定義されています。

mapfilterメソッドと連鎖させることで目的の処理を実現できます。

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には平均を算出するメソッドはありません。したがって、与えられたIntListの平均を算出する関数リテラルavgを定義しています。この関数リテラルは匿名関数としてfilterメソッドに渡されます。

Java 8での関数連鎖

Java 8のコレクションクラスがどのような高階関数を提供するのか現時点では明確ではありません。Filtermapがサポートされる可能性は高いです。最初に示した関数連鎖の例をJava 8で書くと下記のようになるでしょう。

List photos = 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’キーワードがあるので、PhotoLoggableから派生し、他のクラスは継承できないと思えますが、正しくありません。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

VirtualExtensionMethods

著者について

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#にも親しんでいます。

この記事に星をつける

おすすめ度
スタイル

こんにちは

コメントするには InfoQアカウントの登録 または が必要です。InfoQ に登録するとさまざまなことができます。

アカウント登録をしてInfoQをお楽しみください。

あなたの意見をお聞かせください。

HTML: a,b,br,blockquote,i,li,pre,u,ul,p

このスレッドのメッセージについてEmailでリプライする
コミュニティコメント

typo by Takeshi Miyakawa

> Appendix
> Java 8の情報は主に書きのプレゼンを参考にしました。

-> Java 8の情報は主に下記のプレゼンを参考にしました。

HTML: a,b,br,blockquote,i,li,pre,u,ul,p

このスレッドのメッセージについてEmailでリプライする

HTML: a,b,br,blockquote,i,li,pre,u,ul,p

このスレッドのメッセージについてEmailでリプライする

1 ディスカッション

InfoQにログインし新機能を利用する


パスワードを忘れた方はこちらへ

Follow

お気に入りのトピックや著者をフォローする

業界やサイト内で一番重要な見出しを閲覧する

Like

より多いシグナル、より少ないノイズ

お気に入りのトピックと著者を選択して自分のフィードを作る

Notifications

最新情報をすぐ手に入れるようにしよう

通知設定をして、お気に入りコンテンツを見逃さないようにしよう!

BT