倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Groovy でのコンストラクタあれこれ

今さらかも知れませんが、Groovy でコンストラクタを実装する方法をあれこれ試してみました。 今回試したのは以下のような方法です:

では、それぞれを見ていきましょう。

基本


まずは基本。 デフォルト・コンストラクタコンストラクタ定義は Java と同じ。 名前付き引数は Groovy 用です。

デフォルト・コンストラクタ、名前付き引数

Groovy でクラス定義にコンストラクタを書かなければ、Java と同じようにデフォルト・コンストラクタが自動生成されます。 デフォルト・コンストラクタは引数なしで特に何の処理もしない*1コンストラクタです。 また、Groovy 特有のコンストラクタとして名前付き引数のコンストラクタも自動生成されます。 このコンストラクタには名前をキーとする Map を渡します:

import groovy.transform.ToString

@ToString
class Person1{
    String name
    int age
}

def a = new Person1()    // デフォルト・コンストラクタ
a.with{ name = 'Alberto'; age = 12 }
println a    // Person1(Alberto, 12)

def b = new Person1(name:'Beryl', age:34)    // 名前付き引数
println b    // Person1(Beryl, 34)

コンストラクタ定義

次は Java のようにコンストラクタを定義した場合。 まぁ、Java となんら変わりません:

import groovy.transform.*

@ToString
class Person2{
    String name
    int age
    
    Person2(String name, int age){
        this.name = name
        this.age = age
    }
}

def c = new Person2('Chris', 56)
println c    // Person2(Chris, 56)
//def c1 = new Person2()  //  例外
//def c2 = new Person2(name:'Chris', age:56)  //  例外

アノテーションによるAST 変換


次は Groovy 1.8 あたりから導入された、アノテーションによる AST 変換を使った、よくあるコンストラクタ実装の自動生成を行う方法。

の2つがコンストラクタを自動生成するアノテーションです。 どちらも groovy.transform パッケージに含まれています。

@InheritConstructors アノテーション

@Inheritconstructors アノテーションスーパークラスコンストラクタと同じ引数、同じ処理のコンストラクタを自動生成します:

import groovy.transform.*

@InheritConstructors
class Person3 extends Person2{}

def d = new Person3('Debby', 78)
println d    // Person2(Debby, 78)
// def d1 = new Person3()  // 例外
// def d2 = new Persion3(name:'Debby', age:78)  // 例外

@TupleConstructor アノテーション

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

import groovy.transform.*

@TupleConstructor @ToString
class Person4{
    String name
    int age
}

def e = new Person4('Ernesto', 90)    // 引数によって順次プロパティを初期化
println e    // Person4(Ernesto, 90)

def f = new Person4()    // 引数なしも OK
f.with{ name = 'Florence'; age = 123 }
println f    // Person4(Florence, 123)

def g = new Person4('Gordon')    // 渡した引数の個数だけプロパティを初期化
g.age = 456
println g    // Person4(Gordon, 456)
  • Person4 は2つのプロパティ name, age を持つので、引数2つの場合は順に name, age を初期化します。
  • 引数がプロパティの個数以下でも OK です。 引数なしでも可。 引数の個数がプロパティの個数より少ない場合は、引数の個数だけ順番にプロパティを初期化します。

メタプログラミング


次はメタクラスを使ったメタプログラミングによるコンストラクタの実装。

  • 実行時メタプログラミング
  • マジック・パッケージ

拡張モジュールを使ってコンストラクタを追加できないかなぁ?と思ったんですが、あんまりうまくいかなさそうだったのでマジック・パッケージを使った方法を試しました。

実行時メタプログラミング

まずは実行時にメタクラスをイジる実行時メタプログラミング。

import groovy.transform.*

@ToString
class Person{
    String name
    int age
}

// メタプログラミング その1
Person.metaClass.constructor = { String name, int age ->
    new Person(name:name, age:age)
}

def h = new Person('Helene', 789)
println h    // Person(Helene, 789)

// メタプログラミング その2
Person.metaClass{
    constructor = { String name -> new Person(name:name, age:100) }
}

def i = new Person('Isaac')
println i    // Person(Isaac, 100)
  • その1ではメタクラスconstructor プロパティに Person オブジェクトを返すクロージャをセットしています。
  • その2でもやっていることは同じ。 Person クラスの static メソッド metaClass() を呼び出しているような書き方。 この書き方では、他にメソッドも追加したいといったときにキレイに書けます。

マジック・パッケージ

最後は、以前にメソッドを追加するのに試したマジック・パッケージを用いる方法。 コンパイルを行わないといけない方法ですが、実行時メタプログラミングの場合のようなメタクラスをイジるコードを呼び出す必要がありません。

まずは以下のような Person クラスが定義されているとしましょう:

package org.sample

@groovy.transform.ToString
class Persion{
    String name
    int age
}

このとき、以下のようなパッケージ、クラス名の MetaClass のサブクラスを作成します:

package groovy.runtime.metaclass.org.sample

import org.sample.Person

class PersonMetaClass extends DelegatingMetaClass{

    PersonMetaClass(MetaClass delegate) {
        super(delegate)
    }

    @Override
    Object invokeConstructor(Object[] args) {
        if(args.size() == 1 && args[0] instanceof String){
            return new Person(name:args[0], age:200)
        }else{
            return super.invokeConstructor(args)
        }
    }
}
  • パッケージ名は groovy.runtime.metaclass の後に、コンストラクタを追加したいクラスのパッケージを続ける
  • クラス名はコンストラクタを追加したいクラスの名前に 'MetaClass' を続ける
  • コンストラクタの処理は invokeConstructor() メソッドをオーバーライド して実装する。 生成したいインスタンスを返り値にする

とします。 前回との違いは、オーバライドするメソッドが invokeMethod() ではなく invokeConstructor() であるところです。 このクラスをコンパイルしてクラスパス上に配置すると、以下のようなコードが実行できます:

import org.sample.Person

def j = new Person('Joyce')
println j    // org.sample.Person(j, 200)

Groovy のコンストラクタ関連の技法で思いついたのはこんなもんでしたが、他によく使いそうなものあったかな? AST 変換を自作したりするのも出来るといいかもしれないけど、今回はパス。

プログラミングGROOVY

プログラミングGROOVY

*1:フィールドを null や 0 で初期化したりはする。