[Kotlin] 全てをopenにするプラグイン vs 必ずfinalなdata class
JPAを利用するときにKotlin All Open Compiler pluginを利用します。
これを利用するとIntelliJ IDEA上で data class
に @Embeddable
アノテーションなどをつけても怒られなくなりました。
ん?data classは必ずfinalなのではないのか?どうなっているのだ!
data class「all-openプラグインなんかに負けるわけないんだから!」
TL;DR
data class「all-openプラグインには勝てなかったょ。。。」
(openかつtoStringなどを持つクラスが出来上がります)
しかしやらないほうがいいです。
理由:
- 将来に渡って挙動が同じとは限らない
- クラスを拡張されたときの各メソッドの挙動が想定外になる
動機
複合主キーを表現するために作成するクラスには equals()
や hashCode()
がほしかった(検索や並び替えを実現するため)
つまりこれができたら嬉しいじゃん!と思った
@Embeddable
data class UserBookReviewPK(
@Column(name = "user_id") var userId: Int,
@Column(name = "book_id") var bookId: Int
) : Serializable {
constructor() : this(0, 0)
}
検証
まずは普通のdata class。
package foo
data class Hoge(val a: Int)
これを IntelliJ IDEA上でバイトコード変換 && Javaコードにデコンパイル すると以下のようになります
package foo;
import kotlin.Metadata;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@Metadata(
mv = {1, 4, 0},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\b\n\u0002\b\u0006\n\u0002\u0010\u000b\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\b\u0086\b\u0018\u00002\u00020\u0001B\r\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0002\u0010\u0004J\t\u0010\u0007\u001a\u00020\u0003HÆ\u0003J\u0013\u0010\b\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u0003HÆ\u0001J\u0013\u0010\t\u001a\u00020\n2\b\u0010\u000b\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\f\u001a\u00020\u0003HÖ\u0001J\t\u0010\r\u001a\u00020\u000eHÖ\u0001R\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006¨\u0006\u000f"},
d2 = {"Lfoo/Hoge;", "", "a", "", "(I)V", "getA", "()I", "component1", "copy", "equals", "", "other", "hashCode", "toString", "", "socialsv.main"}
)
public final class Hoge {
private final int a;
public final int getA() {
return this.a;
}
public Hoge(int a) {
this.a = a;
}
public final int component1() {
return this.a;
}
@NotNull
public final Hoge copy(int a) {
return new Hoge(a);
}
// $FF: synthetic method
public static Hoge copy$default(Hoge var0, int var1, int var2, Object var3) {
if ((var2 & 1) != 0) {
var1 = var0.a;
}
return var0.copy(var1);
}
@NotNull
public String toString() {
return "Hoge(a=" + this.a + ")";
}
public int hashCode() {
return this.a;
}
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof Hoge) {
Hoge var2 = (Hoge)var1;
if (this.a == var2.a) {
return true;
}
}
return false;
} else {
return true;
}
}
}
ここで build.gradle
で allOpen
に指定してある Embeddable
アノテーションを利用します。
package foo
import javax.persistence.Embeddable
@Embeddable
data class Hoge(val a: Int)
トランスパイル↓
package foo;
import javax.persistence.Embeddable;
import kotlin.Metadata;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@Embeddable
@Metadata(
mv = {1, 4, 0},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\b\n\u0002\b\u0006\n\u0002\u0010\u000b\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\b\u0097\b\u0018\u00002\u00020\u0001B\r\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0002\u0010\u0004J\t\u0010\u0007\u001a\u00020\u0003HÆ\u0003J\u0013\u0010\b\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u0003HÆ\u0001J\u0013\u0010\t\u001a\u00020\n2\b\u0010\u000b\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\f\u001a\u00020\u0003HÖ\u0001J\t\u0010\r\u001a\u00020\u000eHÖ\u0001R\u0014\u0010\u0002\u001a\u00020\u0003X\u0096\u0004¢\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006¨\u0006\u000f"},
d2 = {"Lfoo/Hoge;", "", "a", "", "(I)V", "getA", "()I", "component1", "copy", "equals", "", "other", "hashCode", "toString", "", "socialsv.main"}
)
public class Hoge {
private final int a;
public int getA() {
return this.a;
}
public Hoge(int a) {
this.a = a;
}
public final int component1() {
return this.getA();
}
@NotNull
public final Hoge copy(int a) {
return new Hoge(a);
}
// $FF: synthetic method
public static Hoge copy$default(Hoge var0, int var1, int var2, Object var3) {
if (var3 != null) {
throw new UnsupportedOperationException("Super calls with default arguments not supported in this target, function: copy");
} else {
if ((var2 & 1) != 0) {
var1 = var0.getA();
}
return var0.copy(var1);
}
}
@NotNull
public String toString() {
return "Hoge(a=" + this.getA() + ")";
}
public int hashCode() {
return this.getA();
}
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof Hoge) {
Hoge var2 = (Hoge)var1;
if (this.getA() == var2.getA()) {
return true;
}
}
return false;
} else {
return true;
}
}
}
というわけで final
が外れていて、かつ data class
らしく toString()
equals()
などのメソッドが実装されています。
問題
結論でも書いたとおり、これはやらないほうが良いと思います。問題は2つあります;
挙動は変わる可能性がある
KotlinのIssueにこの話が上がっていました
ここではdata classがopenになる事実を認めながらも
However, I agree with @Alexey Belkov on that breaking data class contracts will eventually bring more harm than good.
と非推奨であることが述べられています。どちらかというと後述の問題点を指摘しているものですが、仕様が変更になる可能性も全然あると思います。
各メソッドの挙動が想定外になる
そもそも data class
に生えてくる hashCode()
toString()
などのメソッドは final
だからこそ非明示的に作成されても安心して利用できます。
例えば、finalでないクラスが実行中に拡張された場合、例えばメンバの追加が行われたりすると copy()
の挙動は不正確になります。 hashCode()
が正しく実装され直されるかもわかりません。
〆
というわけで data class
を無理やり open
にするメリットはない、可能だけど、という学習になりました。
Discussion