手軽に入れ物を作る場合便利

Kotlin にはデータクラスという JavaBeans のようにデータを入れることに特化したクラスを簡単に作成するための仕組みがある。 詳しくは公式リファレンスを参照すればよいが、簡単に書くと以下を自動で用意してくれる:

  • equals() / hashCode()
  • "User(name=John, age=42)" 形式の toString()
  • 宣言した順番でプロパティに対応する componentN() 関数
  • copy() 関数

尚、上記の恩恵を受けられるのはプライマリコンストラクタに指定したプロパティのみということに注意が必要である。 クラスの本文に書いたフィールドに関しては一切データクラスの影響を受けない。 以下それを検証する。

以下のような Kotlin というデータクラスが定義されているものとする:

// a, b, c, d の 4 つのプロパティを受け取るプライマリコンストラクタ
data class Kotlin(var a: Int, var b: Int = 0, val c: Int, val d: Int = 0) {
    var e: Int = 0  // ただのフィールド (再代入可)
    val f: Int = 0  // ただのフィールド (再代入不可)
}

この Kotlin クラスを Java 側から使ってみる:

public class Java {
    public static void main(String...args) {
        final Kotlin kotlin = new Kotlin(0, 0, 0, 0);
        kotlin.setA(1);
        kotlin.setB(1);
        kotlin.setE(1);

        // プライマリコンストラクタに指定してあるフィールドのみ toString() の対象になる
        System.out.println(kotlin);  // Kotlin(a=1, b=1, c=0, d=0)

        // プライマリコンストラクタに指定してあるフィールドのインスタンスの equals() がすべて true ならば true
        final Kotlin kotlin2 = new Kotlin(1, 1, 0, 0);
        kotlin2.setE(2);  // 関係ない値を違う値にする
        System.out.println(kotlin2);  // Kotlin(a=1, b=1, c=0, d=0)
        System.out.println(kotlin.equals(kotlin2));

        kotlin2.setA(2);  // 関係ある値を違う値にしてみる
        System.out.println(kotlin.equals(kotlin2));  // false

        // コピーしても関係ない値はコピーされない
        System.out.println(kotlin2.getE());  // 先ほど変更したので 2
        final Kotlin kotlin3 = kotlin2.copy(3, 3, 3, 3);  // Kotlin で呼ぶと任意のプロパティのみ変更可
        System.out.println(kotlin3.getE());  // 2 ではなく初期値の 0
    }
}

デフォルトコンストラクタの作成条件

前述の Kotlin クラスでは引数なしのコンストラクタ、いわゆるデフォルトコンストラクタが作成されない。 データクラスにおけるデフォルトコンストラクタの作成条件はプライマリコンストラクタのすべてのプロパティに初期値が存在することとなっている。 つまり前述の Kotlin クラスを以下のように書き換える:

// a, b, c, d すべてに初期値を設定
data class Kotlin(var a: Int = 0, var b: Int = 0, val c: Int = 0, val d: Int = 0) {
    var e: Int = 0
    val f: Int = 0
}

するとコンストラクタがデフォルトコンストラクタとプライマリコンストラクタの 2 種に増えている:

public class Java {
    public static void main(String...args) {
        final Kotlin kotlin = new Kotlin();  // デフォルトコンストラクタを呼ぶ
        final Kotlin kotlin2 = new Kotlin(1, 2, 3, 4);  // 従来のすべての引数ありコンストラクタも作成される
    }
}

尚、以下のようにデフォルトコンストラクタを明示的に宣言しても良い:

data class Kotlin(var a: Int, var b: Int = 0, val c: Int, val d: Int = 0) {
    constructor(): this(0, 0, 0, 0)  // 明示的なデフォルトコンストラクタ
    var e: Int = 0
    val f: Int = 0
}

JPA Entity ではデフォルトコンストラクタの定義が必要

ここからは Spring Boot での JPA の話である。 例えば以下のようにデータクラスで Entity を定義する:

@Entity
@Table(name = "posts")
data class Post(
        @Id @GeneratedValue
        var id: Int,
        var date: String,
        var name: String,
        var body: String,
        var enabled: Boolean,
        var created: Date,
        var modified: Date
)

これを PostRepository から DB アクセスを行うと以下のエラーが表示される:

org.hibernate.InstantiationException: No default constructor for entity:  : com.kojion.entity.Post

先ほどの教訓から解決法は明らかだ。 以下のようにすべてデフォルト値を指定してやれば良い:

@Entity
@Table(name = "posts")
data class Post(
        @Id @GeneratedValue
        var id: Int = 0,
        var date: String = "",
        var name: String = "",
        var body: String = "",
        var enabled: Boolean = false,
        var created: Date = Date(),
        var modified: Date = Date()
)

toString() が循環呼び出しされてしまう場合データクラスの対象外にする

例えば以下のように相互にアソシエーションを張っている場合に起きる:

// Post は複数の Tag を持つ
@Entity
@Table(name = "posts")
data class Post(
        @Id @GeneratedValue
        var id: Int = 0,
        @ManyToMany
        @JoinTable(name = "posts_tags", joinColumns = [JoinColumn(name="post_id")], inverseJoinColumns = [JoinColumn(name="tag_id")])
        var tags: List<Tag> = arrayListOf()
)

// Tag も複数の Post を持つ
@Entity
@Table(name = "tags")
data class Tag(
        @Id
        @GeneratedValue
        var id: Int = 0,
        @ManyToMany(mappedBy = "tags")
        var posts: List<Post> = arrayListOf()
)

この例だと PostTag が中間テーブル posts_tags を通して多対多のアソシエーションが張られている。 ここで同じように PostRepository から取得した Entity を出力しようとすると java.lang.StackOverflowError: null とクラッシュする。 PosttoString() しようとしてフィールドの List<Tag> に対しても toString() を試み、更に Tag にも子の List<Post> があり……というわけである。

この例の場合 Post が何の Tag を持つかは見たいが Tag が何の Post を持っているかはそこまで見たくない (必要ならば別途取ってくれば良い)。 そこで Tag 側の @ManyToMany 定義されているプロパティをプライマリコンストラクタの範囲から出すことで TagtoString() しようとした時に子の List<Post> を見に行かなくなる:

@Entity
@Table(name = "tags")
data class Tag(
        @Id
        @GeneratedValue
        var id: Int = 0
) {
    @ManyToMany(mappedBy = "tags")
    var posts: List<Post> = arrayListOf()
}