CLOVER🍀

That was when it all began.

Scalaから、Javaのインターフェースに定義されたeq(Object)メソッドを呼び出す方法を考える

以前、こんなエントリを書きました。

Scalaから、Javaのインターフェースに定義されたeq(Object)メソッドを呼び出せない?
http://d.hatena.ne.jp/Kazuhira/20131215/1387109792

Scalaでは、AnyRefに

  final def eq(that: AnyRef): Boolean

というメソッドが定義されているのですが、Javaでインターフェースに同名のメソッドがあった場合はうまく呼び出せないという話でした。

Stack Overflowでも。

How to call T eq(Object) method of Java interface from Scala?
http://stackoverflow.com/questions/7263861/how-to-call-t-eqobject-method-of-java-interface-from-scala

なお、インターフェースを実装したクラスにダウンキャストしてあげると、ちゃんと呼び出すことができます。つまり、インターフェースの型で扱ったままだとダメなのです。

個人的には、Javaのクエリを組み立てるDSLでこれにぶつかり、代替のメソッド(意味違うけど)に逃げてることが多いです…。

で、実際解決するとしたらどうしようかなぁ〜ということで、大して考えてませんが、対応策を。

やっぱり、Implicit Class+Value Class?

対象とするJavaのインターフェースとしては、前回同様こんなのを考えます。
src/main/java/HasEqInterface.java

public interface HasEqInterface {
    String eq(Object target);
}

引数の型がObject、戻り値の型がStringのeqメソッドです。

その実装クラス。
src/main/java/HasEqImpl.java

public class HasEqImpl implements HasEqInterface {
    private String value;

    public HasEqImpl(String value) {
        this.value = value;
    }

    @Override
    public String eq(Object target) {
        return Boolean.valueOf(value.equals(target.toString()))
            .toString();
    }
}

で、これに対してこんなScalaコードを書くと
src/test/scala/AvoidHasEqSpec.scala

import org.scalatest.FunSpec
import org.scalatest.Matchers._

class AvoidHasEqSpec extends FunSpec {
  describe("avoid has eq") {
    it("test") {
      val hasEq: HasEqInterface = new HasEqImpl("Hello World")
      val result: String = hasEq.eq("Hello World")

      result should be ("true")
    }
  }
}

まあ、コンパイルエラーです。

> test
[info] Compiling 1 Scala source to /xxxxx/target/scala-2.10/test-classes...
[error] /xxxxx/src/test/scala/AvoidHasEqSpec.scala:10: type mismatch;
[error]  found   : Boolean
[error]  required: String
[error]       val result: String = hasEq.eq("Hello World")
[error]                                    ^
[error] one error found
[error] (test:compile) Compilation failed
[error] Total time: 1 s, completed 2014/03/01 16:49:23

ちなみに、こんな形で推論させると

      val result = hasEq.eq("Hello World")

警告されます。

[info] Compiling 1 Scala source to /xxxxx/target/scala-2.10/test-classes...
[warn] /xxxxx/src/test/scala/AvoidHasEqSpec.scala:10: HasEqInterface and String are unrelated: they will most likely never compare equal
[warn]       val result = hasEq.eq("Hello World")
[warn]                            ^

ムリに動かしても、Booleanが戻ってくることになりますが。

で、このままだとどうやってもHasEqInterface#eqメソッドを呼べないので(ここでは実体の型にダウンキャストすることは、考えないことにします)、Javaでヘルパー的なものを作ります。
src/main/java/HasEqHelper.java

public class HasEqHelper {
    public static String eqDelegate(HasEqInterface hasEq, Object target) {
        return hasEq.eq(target);
    }
}

この後で作るScalaのブリッジコードから呼べるように、「eqDelegate」という名前に。

これに対して、ScalaでImplicit ClassとValue Classで変換を被せます。
src/main/scala/HasEqHelperScala.scala

object HasEqHelperScala {
  implicit class HasEqWrapper(val underlying: HasEqInterface) extends AnyVal {
    def equal(target: AnyRef): String =
      HasEqHelper.eqDelegate(underlying, target)
  }
}

eqメソッドだと結局ダメなので、ここでは「equal」と別名にすることにしました。

src/test/scala/AvoidHasEqSpec.scala

import org.scalatest.FunSpec
import org.scalatest.Matchers._

import HasEqHelperScala._

class AvoidHasEqSpec extends FunSpec {
  describe("avoid has eq") {
    it("test") {
      val hasEq: HasEqInterface = new HasEqImpl("Hello World")
      //val result: String = hasEq.eq("Hello World")
      val result: String = hasEq.equal("Hello World")

      result should be ("true")
    }
  }
}

これで、動かすことができます。

> test
[info] Compiling 1 Java source to /xxxxx/target/scala-2.10/classes...
[info] Compiling 1 Scala source to /xxxxx/target/scala-2.10/test-classes...
[info] AvoidHasEqSpec:
[info] avoid has eq
[info] - test
[info] Run completed in 348 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 3 s, completed 2014/03/01 17:03:03

まあ、実際困ったらこんな逃げ方をするんですかねぇ…。

一応確認として、sbtでの設定時に

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-Xprint:jvm")

として途中のコードを確認。

うーん。

  <synthetic> object HasEqWrapper extends Object {
    final def equal$extension($this: HasEqInterface, target: Object): String = HasEqHelper.eqDelegate($this, target);
    final <synthetic> def hashCode$extension($this: HasEqInterface): Int = $this.hashCode();
    final <synthetic> def equals$extension($this: HasEqInterface, x$1: Object): Boolean = {
  case <synthetic> val x1: Object = x$1;
  case5(){
    if (x1.$isInstanceOf[HasEqWrapper]())
      matchEnd4(true)
    else
      case6()
  };

あとは、呼んでるところ。

      val result: String = HasEqWrapper.this.equal$extension(HasEqHelperScala.HasEqWrapper(hasEq), "Hello World");

新しいインスタンスは作ってないから、大丈夫そうですね。

Javaでコードを書かずに回避はできない気がしますが、いざとなったらこんな感じで対処でしょう。