Groovyのマップベースコンストラクターについて

マップベースコンストラクターを使っていて気になったことがあったので書きます。

マップベースコンストラクターとは

Groovyではマップベースコンストラクターを使うことができます。
名前付き引数のコンストラクターとも呼ばれます。

マップでフィールド名と値の組み合わせをコンストラクターの引数として渡すことで、
その渡された値で対応するフィールドが初期化されます。

class Person {
    String name
    Integer age
}

def person = new Person(name: 'Taro', age: 25)

マップベースコンストラクターと引数なしコンストラクター

ここで引数なしのコンストラクターを宣言してみます。

class Person {
    String name
    Integer age

    Person() {
        age = 10
    }
}

ageは引数なしコンストラクターで初期化するようにしたので、
nameだけマップベースコンストラクターで初期化してみましょう。

def person = new Person(name: 'Taro')

assert person.name == 'Taro'
assert person.age == 10

この動きは最初奇妙に感じました。
なぜなら、マップベースコンストラクターを呼び出したことで、
引数なしのコンストラクターまで呼び出されるとは思っていなかったからです。

マップベースコンストラクターが利用できるのは引数なしのコンストラクターが存在するときだけです。
この条件が頭に入っていなかったため、奇妙に感じたのだと思います。

マップベースコンストラクターの動き

マップベースコンストラクターを呼び出すと、どのタイミングで引数なしのコンストラクターが呼ばれるのでしょう?

このPersonクラスをgroovycでコンパイルして、javapコマンドで中身を見てみましょう。

class Person {
    String name
    Integer age
}
Compiled from "Person.groovy"
public class Person implements groovy.lang.GroovyObject {
  public static transient boolean __$stMC;
  public static long __timeStamp;
  public static long __timeStamp__239_neverHappen1371571621475;
  public Person();
  public java.lang.Object this$dist$invoke$1(java.lang.String, java.lang.Object);
  public void this$dist$set$1(java.lang.String, java.lang.Object);
  public java.lang.Object this$dist$get$1(java.lang.String);
  protected groovy.lang.MetaClass $getStaticMetaClass();
  public groovy.lang.MetaClass getMetaClass();
  public void setMetaClass(groovy.lang.MetaClass);
  public java.lang.Object invokeMethod(java.lang.String, java.lang.Object);
  public java.lang.Object getProperty(java.lang.String);
  public void setProperty(java.lang.String, java.lang.Object);
  public static void __$swapInit();
  static {};
  public java.lang.String getName();
  public void setName(java.lang.String);
  public java.lang.Integer getAge();
  public void setAge(java.lang.Integer);
  public void super$1$wait();
  public java.lang.String super$1$toString();
  public void super$1$wait(long);
  public void super$1$wait(long, int);
  public void super$1$notify();
  public void super$1$notifyAll();
  public java.lang.Class super$1$getClass();
  public java.lang.Object super$1$clone();
  public boolean super$1$equals(java.lang.Object);
  public int super$1$hashCode();
  public void super$1$finalize();
  static java.lang.Class class$(java.lang.String);
}

おっとマップベースコンストラクターがいません。

どうやらマップベースコンストラクターはクラスのコンストラクターとして生成されるのではなく、
Groovyの処理系でよしなにされているということになります。

これについて調べていると、Map-based constructor unable to set final fields?を見つけました。

この中でGuillaume氏が

Think of new Foo(t: ‘test’) as new Foo() then foo.setText('test’), and this more obviously not allowed.

と言っています。

つまりマップベースコンストラクターの役割を簡単にいうと、
「引数なしのコンストラクターで生成したインスタンスにマップで渡された値をバインドする」というところですかね。

finalなフィールドとマップベースコンストラクター

なので、このメールの議題となっている通り、
finalなフィールドにはマップベースコンストラクターから値をセットできず、
groovy.lang.ReadOnlyPropertyExceptionがスローされます。

class Person {
    final String name
    Integer age
}

def person = new Person(name: 'Taro', age: 20) // -> groovy.lang.ReadOnlyPropertyException

@TupleConstructorアノテーション

ここではfinalなフィールドへ簡単にコンストラクターで値をバインドする方法として、
@TupleConstructorアノテーションによるコンストラクターの生成が提案されています。

@TupleConstructorアノテーションは、渡された引数で順次フィールドを初期化していくコンストラクターを生成します。

@groovy.transform.TupleConstructor
class Person {
    final String name
    Integer age
}

def person = new Person('Taro', 20)

assert person.name == 'Taro'
assert person.age == 20

この場合、マップベースコンストラクターとは違い、AST変換によってクラスに対してコンストラクターが生成されます。

@groovy.transform.TupleConstructor
class Person {
    String name
    Integer age
}
Compiled from "Person.groovy"
public class Person implements groovy.lang.GroovyObject {
  public static transient boolean __$stMC;
  public static long __timeStamp;
  public static long __timeStamp__239_neverHappen1371573480352;
  public Person(java.lang.String, java.lang.Integer);
  public Person(java.lang.String);
  public Person();
  public java.lang.Object this$dist$invoke$1(java.lang.String, java.lang.Object);
...(略)

これならfinalなフィールドも初期化可能ですね。