手軽に入れ物を作る場合便利
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()
)
この例だと Post
と Tag
が中間テーブル posts_tags
を通して多対多のアソシエーションが張られている。
ここで同じように PostRepository
から取得した Entity を出力しようとすると java.lang.StackOverflowError: null
とクラッシュする。
Post
を toString()
しようとしてフィールドの List<Tag>
に対しても toString()
を試み、更に Tag
にも子の List<Post>
があり……というわけである。
この例の場合 Post
が何の Tag
を持つかは見たいが Tag
が何の Post
を持っているかはそこまで見たくない (必要ならば別途取ってくれば良い)。
そこで Tag
側の @ManyToMany
定義されているプロパティをプライマリコンストラクタの範囲から出すことで Tag
を toString()
しようとした時に子の List<Post>
を見に行かなくなる:
@Entity
@Table(name = "tags")
data class Tag(
@Id
@GeneratedValue
var id: Int = 0
) {
@ManyToMany(mappedBy = "tags")
var posts: List<Post> = arrayListOf()
}