seratch's weblog in Japanese

About Scala, Java and Ruby programming in Japaense. If you need English information, go to http://blog.seratch.net/

Scala School意訳(Java + Scala)

http://twitter.github.com/scala_school/java.html

以下は私の方でtypoや表示崩れを直したものです。

https://github.com/seratch/scala_school/blob/master/web/_posts/2011-05-11-lesson.textile

誤訳などありましたら、お手数ですが、ご指摘いただければ幸いです。

Javap

javapはJDKに同梱されているツールです。JREではありませんよ。大違いです。

javapはクラス定義をデコンパイルして中身がどうなっているかを表示します。使い方は極めてシンプルです。

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap MyTrait
Compiled from "Scalaisms.scala"
public interface com.twitter.interop.MyTrait extends scala.ScalaObject{
    public abstract java.lang.String traitName();
    public abstract java.lang.String upperTraitName();
}

ハードコアなあなたなら、バイトコードを読むことが出きるかもしれません。

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap -c MyTrait\$class
Compiled from "Scalaisms.scala"
public abstract class com.twitter.interop.MyTrait$class extends java.lang.Object{
public static java.lang.String upperTraitName(com.twitter.interop.MyTrait);
  Code:
   0:	aload_0
   1:	invokeinterface	#12,  1; //InterfaceMethod com/twitter/interop/MyTrait.traitName:()Ljava/lang/String;
   6:	invokevirtual	#17; //Method java/lang/String.toUpperCase:()Ljava/lang/String;
   9:	areturn

public static void $init$(com.twitter.interop.MyTrait);
  Code:
   0:	return

}

なぜJavaの世界だと動かせないのか不思議に思い始めたら、javapを手に取ってみましょう!

Classes

ScalaのクラスをJavaから使うときに、四つの考慮すべき大きな要素があります。

- Class parameters
- Class vals
- Class vars
- Exceptions

いろんな要素を詰め込んだ単純なScalaのクラスをつくってみるとしましょう。

package com.twitter.interop

import java.io.IOException
import scala.throws
import scala.reflect.{BeanProperty, BooleanBeanProperty}

class SimpleClass(name: String, val acc: String, @BeanProperty var mutable: String) {
  val foo = "foo"
  var bar = "bar"
  @BeanProperty
  val fooBean = "foobean"
  @BeanProperty
  var barBean = "barbean"
  @BooleanBeanProperty
  var awesome = true

  def dangerFoo() = {
    throw new IOException("SURPRISE!")
  }

  @throws(classOf[IOException])
  def dangerBar() = {
    throw new IOException("NO SURPRISE!")
  }
}
Class parameters
- デフォルトでは、クラスのパラメータはJavaの世界では事実上コンストラクタ引数となります。このことは、クラスの外側ではこれらのパラメータにアクセスできないということを意味します。
- クラスのパラメータををval/varとして宣言することはこのコードと同じことです。

class SimpleClass(acc_: String) {
  val acc = acc_
}

こうすることで、その他のvalと同様にJavaのコードから(accに)アクセスすることができるようになります。
Vals
val定義にはJavaからアクセスするため定義されたメソッドが生成されます。
val "foo"の値には "foo()"というメソッドでアクセスすることができます。
Vars
var定義は_$eqというメソッド定義を生成します。このように呼び出すことができます。

foo$_eq("newfoo");
BeanProperty
val定義に@BeanPropertyアノテーションを付与することができます。
これによってPOJO風なgetter/setter定義のアクセサが生成されます。
もしisFooという変形が欲しければ、@BooleanBeanPropertyアノテーションを使ってください。
そうすると、見にくかった foo$_eq がこのようになります。

setFoo("newfoo");
getFoo();
Exceptions
Scalaにはチェック例外がありません。ここではあまり深入りはしないでおく哲学的論争ではありますが、もし例外をJavaでキャッチしたい場合は問題になるでしょう。

dangerFooとdangerBarの定義は、これのデモです。Javaのコードにおいて、このようなことができないのです。

// 例外が消去されている!
try {
  s.dangerFoo();
} catch (IOException e) {
  // UGLY
}

Javaコンパイラはs.dangerFooはIOExceptionをthrowしませんとエラーを吐くでしょう。
Throwableをキャッチするようにして場当たり的にハックすることもできますが、まずいやり方ですね。

その代わりに、善良なるScala市民であるためのdangerBarにやっているような@throwsアノテーションを使うという礼儀正しいアイデアがあります。
これによってJavaの世界でチェック例外を使い続けることができます。

@throws(classOf[IOException])
def dangerBar() = {
  throw new IOException("NO SURPRISE!")
}
Further Reading
Javaとの相互運用のために提供されるScalaのアノテーションのすべての一覧はここで見ることができます。

http://www.scala-lang.org/node/106

Traits

どうやって(Javaにおける)インタフェースと実装を取得しているのでしょうか?シンプルなトレイト定義を見てみましょう。

trait MyTrait {
  def traitName:String
  def upperTraitName = traitName.toUpperCase
}

このトレイトは一つの抽象メソッドを持っていて(traitName)、一つの実装されたメソッドを持っています(upperTraitName)。
Scalaは何を生成してくれるのでしょう? それはMyTraitという名前のインタフェースとMyTrait$classという名前のコンパニオン実装です。

MyTraitの実装はあなたの予想通りだと思います。

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap MyTrait
Compiled from "Scalaisms.scala"
public interface com.twitter.interop.MyTrait extends scala.ScalaObject{
    public abstract java.lang.String traitName();
    public abstract java.lang.String upperTraitName();
}

MyTrait$classの実装は、もっと面白いことになっています。

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap MyTrait\$class
Compiled from "Scalaisms.scala"
public abstract class com.twitter.interop.MyTrait$class extends java.lang.Object{
    public static java.lang.String upperTraitName(com.twitter.interop.MyTrait);
    public static void $init$(com.twitter.interop.MyTrait);
}

MyTrait$classはMyTraitのインスタンスを受け取るstaticメソッドだけを持っています。
これはトレイトがJavaではどのように展開されるかを知るためのヒントになりますね。

とりあえず、以下のような試みをしてみます。

package com.twitter.interop;

public class JTraitImpl implements MyTrait {
    private String name = null;

    public JTraitImpl(String name) {
        this.name = name;
    }

    public String traitName() {
        return name;
    }
}

すると、以下のようなコンパイルエラーが発生しました。

[info] Compiling main sources...
[error] /Users/mmcbride/projects/interop/src/main/java/com/twitter/interop/JTraitImpl.java:3: com.twitter.interop.JTraitImpl is not abstract and does not override abstract method upperTraitName() in com.twitter.interop.MyTrait
[error] public class JTraitImpl implements MyTrait {
[error]        ^

自分で実装することもできますが、それは卑劣なやり方です。

package com.twitter.interop;

public String upperTraitName() {
  return MyTrait$class.upperTraitName(this);
}

このような呼び出しは、生成されたScalaの実装に移譲することができます。
私たちは単にメソッドをoverrideすることができます。

Objects

(シングルトン)オブジェクトは、Scalaでstaticなメソッドや、シングルトンパターンを実装するための方法です。
これをJavaから使うのは、少し奇妙な感じになります。
オブジェクトを使うための文体的にパーフェクトな方法はありませんが、Scala 2.8だとそんなにひどくはありません。

Scalaのオブジェクトは、最後に"$"を引きずった名前のクラスにコンパイルされます。
それではクラスとコンパニオンオブジェクトをセットアップしてみましょう。

class TraitImpl(name: String) extends MyTrait {
  def traitName = name
}

object TraitImpl {
  def apply = new TraitImpl("foo")
  def apply(name: String) = new TraitImpl(name)
}

私たちはJavaからこのように素朴にアクセスすることができます。

MyTrait foo = TraitImpl$.MODULE$.apply("foo");

今あなたは自分自身に問いかけているでしょう。なんてこった!それは正常なリアクションです。
それでは実際にTraitImpl$の内部が実際にどうなっているか見てみましょう。

local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap TraitImpl\$
Compiled from "Scalaisms.scala"
public final class com.twitter.interop.TraitImpl$ extends java.lang.Object implements scala.ScalaObject{
    public static final com.twitter.interop.TraitImpl$ MODULE$;
    public static {};
    public com.twitter.interop.TraitImpl apply();
    public com.twitter.interop.TraitImpl apply(java.lang.String);
}

実はstaticメソッドは存在しません。代わりにMODULE$と名づけられたstaticなメンバーがいます。
メソッドの実装はこのメンバーに移譲されています。
これによって醜いアクセスの仕方になっていますが、MODULE$を使うことを知っていれば動作させることはできます。
Forwarding Methods
Scala 2.8では、オブジェクトの扱いがかなりやりやすくなりました。
コンパニオンオブジェクトのあるクラスであれば、2.8のコンパイラはforwardするメソッドをコンパニオンクラスに生成します。
なので、もし2.8でビルドすれば、TraitImplオブジェクトのメソッドにはこのようにアクセスすることができます。

MyTrait foo = TraitImpl.apply("foo");

Closures Functions

Scalaの最も重要な特長の一つは、一級市民として関数を扱えることです。
関数を引数としてとるいくつかのメソッドが定義されたクラスを定義してみましょう。

class ClosureClass {
  def printResult[T](f: => T) = {
    println(f)
  }

  def printResult[T](f: String => T) = {
    println(f("HI THERE"))
  }
}

Scalaではこのように呼び出すことができます。

val cc = new ClosureClass
cc.printResult { "HI MOM" }

Javaではそう簡単にはいきませんが、そんなにひどいことにはなりません。
ClosureClassが実際にどのようにコンパイルされるかを見てみましょう。

[local ~/projects/interop/target/scala_2.8.1/classes/com/twitter/interop]$ javap ClosureClass
Compiled from "Scalaisms.scala"
public class com.twitter.interop.ClosureClass extends java.lang.Object implements scala.ScalaObject{
    public void printResult(scala.Function0);
    public void printResult(scala.Function1);
    public com.twitter.interop.ClosureClass();
}

そんんあにおぞましいことにはなっていません。「f: => T」が「Function0」に、「f: String => T」が「Function1」に変換されています。
実はScalaはFunction0からFunction22までを定義していますので、このように22個の引数まではサポートされています。そして、22個あれば実際問題十分なはずです。

では、JavaでどうやってFunctionのインスタンスを得ればよいかを知る必要があります。
調べていくと、Scalaが以下のようにして引数に渡せる、AbstractFunction0やAbstractFunction1といったクラスを提供していることがわかるでしょう。

@Test public void closureTest() {
  ClosureClass c = new ClosureClass();
  c.printResult(new AbstractFunction0() {
    public String apply() {
      return "foo";
    }
  });
  c.printResult(new AbstractFunction1<String, String>() {
    public String apply(String arg) {
      return arg + "foo";
    }
  });
}

パラメータ化された引数として、ジェネリクスが使えることに注目してください。