CLOVER🍀

That was when it all began.

ScalaとJavaとインナークラスと

MapReduceを調べていて、少し前のScalaとHadoopを使ったプログラムを書くと、Hadoopがインナークラスをメソッドの引数に強制するので、面倒、という記事を見かけました。

要は、こういうやつです。

override def map(key: LongWritable,
                 value: Text,
                 context: Mapper[LongWritable, Text, Text, IntWritable]#Context) {

型引数も省略できないので、面倒ですよね〜という話でした。

staticでないインナークラスを引数に取る時は、こんな書き方をするのかぁ…と思い、知らなかったのでちょっと試してみることにしました。
*パス依存型と言われるものは、以前にマクロとかで使いましたけど

これを実行した環境は、

  • Scala 2.10.1
  • sbt 0.12.2
  • Java 1.7.0 update 17

となります。

まずはスケープゴートを。

Javaから。

public class JavaClass<T> {
    public T value;

    public JavaClass(T value) {
        this.value = value;
    }

    public class JavaInnerClass {
        public T getValue() { return value; }
    }

    public static class JavaStaticInnerClass { }
}

せっかくなので、型パラメータも入れておきました。

続いて、Scala。

object ScalaOuterObject {

  class ScalaObjectInnerClass { }
}

class ScalaOuterClass[T](value: T) {

  class ScalaInnerClass {
    def get: T = value
  }
}

こちらも、無駄に型パラメータ入り。

これを、それぞれScalaでインスタンス化してみたり、メソッドの引数に取ったりしてみます。

object ScalaWithInnerClass {
  def main(args: Array[String]): Unit = {
    // 普通のJavaクラス呼び出し
    method(new JavaClass("Hello World"))

    // Javaのインナークラス
    val joc = new JavaClass("Hello World")
    method(new joc.JavaInnerClass)
    // 変数に代入
    val jic: JavaClass[String]#JavaInnerClass = new joc.JavaInnerClass

    // JavaのStaticインナークラス
    method(new JavaClass.JavaStaticInnerClass)
    // 変数に代入
    val jsic: JavaClass.JavaStaticInnerClass = new JavaClass.JavaStaticInnerClass


    // Scalaのオブジェクト内のインナークラス
    method(new ScalaOuterObject.ScalaObjectInnerClass)
    // 変数に代入
    val soic: ScalaOuterObject.ScalaObjectInnerClass =
      new ScalaOuterObject.ScalaObjectInnerClass

    // 普通のScalaクラス
    method(new ScalaOuterClass("Hello World"))

    // Scalaクラス内のインナークラス
    val soc = new ScalaOuterClass("Hello World")
    method(new soc.ScalaInnerClass)
    // 変数に代入
    val sic: ScalaOuterClass[String]#ScalaInnerClass = new soc.ScalaInnerClass
  }

  def method[T](arg: JavaClass[T]) {
    println(s"JavaClass => $arg}")
  }
  def method[T](arg: JavaClass[T]#JavaInnerClass) {
    println(s"JavaClass#JavaInnerClass => $arg")
  }
  def method(arg: JavaClass.JavaStaticInnerClass) {
    println(s"JavaClass.JavaStaticInnerClass => $arg")
  }

  def method[T](arg: ScalaOuterObject.ScalaObjectInnerClass) {
    println(s"ScalaOuterObject.ScalaObjectInnerClass $arg")
  }
  def method[T](arg: ScalaOuterClass[T]) {
    println(s"ScalaOuterClass => $arg")
  }
  def method[T](arg: ScalaOuterClass[T]#ScalaInnerClass) {
    println(s"ScalaOuterClass.ScalaInnerClass => $arg")
  }
}

なるほど、staticでないインナークラスの場合は、

外側のクラス#内側のクラス

という表記となるということですね。

なお、オブジェクト内に定義したインナークラスはstaticインナークラスに

public final class ScalaOuterObject
{
    public static class ScalaObjectInnerClass
    {

        public ScalaObjectInnerClass()
        {
        }
    }

}

クラス内に定義した内部インナークラスは、staticではないインナークラスになることも確認できました。

public class ScalaOuterClass
{
    public class ScalaInnerClass
    {

        public Object get()
        {
            return ScalaOuterClass$ScalaInnerClass$$$outer().ScalaOuterClass$$value;
        }
 〜省略〜
}

Javaではstaticでないインナークラスをnewする時に変数に落とさずに書ける方法がありますが、Scalaの場合は

new JavaClass("Hello World").new JavaInnerClass

みたいに書くと

ScalaWithInnerClass.scala:9: identifier expected but 'new' found.
[error]     new JavaClass("Hello World").new JavaInnerClass
[error]                                  ^
[error] one error found

みたいに言われてコンパイルエラーとなりました。

ふーんって感じですね。

で、せっかくなのでJavaからも呼び出してみようかな、と。

これがいけなかった。

とりあえず、試してみます。まずはJavaのクラスから。

new JavaClass<String>("Hello World");
new JavaClass<String>("Hello World").new JavaInnerClass();
new JavaClass.JavaStaticInnerClass();

こっちは、問題ありません。

続いて、Scala。

new ScalaOuterObject.ScalaObjectInnerClass();
new ScalaOuterClass<String>("Hello World").new ScalaInnerClass();

すると…

[error] /xxxxx/scala-with-inner-class/src/main/java/JavaWithInnerClass.java:8: エラー: クラス ScalaOuterClass<T>.ScalaInnerClassのコンストラクタ ScalaInnerClassは指定された型に適用できません。
[error]         new ScalaOuterClass<String>("Hello World").new ScalaInnerClass();
[error]                                                    ^
[error]   期待値: ScalaOuterClass<String>
[error]   検出値: 引数がありません
[error]   理由: 実引数リストと仮引数リストの長さが異なります
[error]   Tが型変数の場合:
[error]     クラス ScalaOuterClassで宣言されているT extends Object
[error] エラー1個

ん?なんで??逆コンパイルした感じ、普通のJavaのインナークラスだったよ??

コンストラクタは、Java同様外側のインスタンスを引数に取るものを持っていたので…ものは試しと突っ込んでみました。

new ScalaOuterObject.ScalaObjectInnerClass();
ScalaOuterClass<String> so = new ScalaOuterClass<String>("Hello World");
so.new ScalaInnerClass(so);

すると、恐ろしいことにコンパイルが通りました。

で、実行すると…

[error] (run-main) java.lang.NoSuchMethodError: ScalaOuterClass$ScalaInnerClass.<init>(LScalaOuterClass;LScalaOuterClass;)V
java.lang.NoSuchMethodError: ScalaOuterClass$ScalaInnerClass.<init>(LScalaOuterClass;LScalaOuterClass;)V
	at JavaWithInnerClass.main(JavaWithInnerClass.java:9)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:601)
[trace] Stack trace suppressed: run last compile:run for the full output.
java.lang.RuntimeException: Nonzero exit code: 1
	at scala.sys.package$.error(package.scala:27)
[trace] Stack trace suppressed: run last compile:run for the full output.
[error] (compile:run) Nonzero exit code: 1

ですよねー。

なんでしょう??コレ。

何がいけないんでしょう?いろいろ考えて、いきなり型パラメータを使うなんて半端なことをやったのがマズかったのではと思い、Scalaでもっと簡単なクラスを定義。

class Outer {
  class Inner
}

Javaから呼び出してみます。

new Outer().new Inner();

なんと、これは普通にコンパイルが通りました。動作もします。

…なら、今度は型パラメータを入れてみます。

class Outer[T] {
  class Inner
}

Java側は、何も変えずにコンパイル。

[error] /xxxxx/scala-with-inner-class/src/main/java/JavaWithInnerClass.java:23: エラー: クラス Outer<T>.Innerのコンストラクタ Innerは指定された型に適用できません。
[error]         new Outer().new Inner();
[error]                     ^
[error]   期待値: Outer
[error]   検出値: 引数がありません
[error]   理由: 実引数リストと仮引数リストの長さが異なります
[error]   Tが型変数の場合:
[error]     クラス Outerで宣言されているT extends Object
[error] エラー1個
[error] (compile:compile) javac returned nonzero exit code
[error] Total time: 3 s, completed 2013/03/18 0:28:49

えー。

まあ、JavaからScalaのインナークラスを呼ぶなんて(しかも、オブジェクト内のインナークラスじゃない方)、滅多なことじゃ無い気がしますけど…。
普段使う時は、きっと外側のクラスのインスタンスをファクトリとして使うことが多いはず、と考えてみます。