はじめに
Kotlinのデータクラスがすごく便利でよく使うのですが、
クラスとデータクラスの違いがなんとなく理解になっているんですよね…
なんとなく理解で違いを思い出しては忘れを繰り返すのは
非常に時間の無駄だな〜と感じていい機会なので違いをまとめたいと思います。
クラスとデータクラスの違いとは何か?
クラスとデータクラスではequals,hashCode,toString,componentN,copyの関数の仕様が違います。
次の表に5つの関数の実装の内容をクラスとデータクラスごとにまとめてみました。
表を見てもらうとわかりますがクラスとデータクラスで関数の実装がけっこう違います。
No | 関数名称 | データクラス | クラス |
---|---|---|---|
1 | equals() | 同じプロパティ値を持ったインスタンスか比較する | 同じインスタンスであるか比較する |
2 | hashCode() | インスタンスが持つプロパティ値からHashCodeが出力される。 | インスタンスごとにユニークなHashCodeが出力される。 |
3 | toString() | "User(name=John, age=42)"形式の文字列を返す | "advanced.dataclass.ToStringSampleKt$main$User@5e481248"形式の文字列を返す |
4 | componentN() | クラスに宣言したプロパティにcomponent1,component2でアクセスできる | 実装なし |
5 | copy() | クラスのインスタンスをコピーする。 | 実装なし |
equals()
equal()の動作を確認するために次のコードを動作させてみます。
次のようにクラスでは同じインスタンスであるか比較しており、
データクラスでは同じ値のインスタンスであるか比較していることがわかります。
Sample Code
fun main(args: Array<String>) {
class User(val name: String, val age: Int)
data class UserData(val name: String, val age: Int)
val user1 = User("Foobar", 100)
val user2 = User("Foobar", 100)
println("クラス 同じインスタンス : " + user1.equals(user1))
println("クラス 別のインスタンス : " + user1.equals(user2))
val userData1 = UserData("Foobar", 100)
val userData2 = UserData("Foobar", 100)
println("データクラス 同じインスタンス : " + userData1.equals(userData1))
println("データクラス 別のインスタンス : " + userData1.equals(userData2))
}
Output
クラス 同じインスタンス : true
クラス 別のインスタンス : false
データクラス 同じインスタンス : true
データクラス 別のインスタンス : true
hashCode()
hashCode()の動作を確認するために次のコードを動作させてみます。
次のようにクラスではインスタンスごとにユニークなHashCodeが出力され、
データクラスではインスタンスが持つプロパティ値からHashCodeが出力されます。
そのためデータクラスでは同じプロパティ値を持つ場合、同じHashCodeが出力されます。
Sample Code
fun main(args: Array<String>) {
class User(val name: String, val age: Int)
data class UserData(val name: String, val age: Int)
val user1 = User("Foobar", 100)
val user2 = User("Foobar", 100)
val user3 = User("Barfoo", 0)
println("user1のhashCode : " + user1.hashCode())
println("user2のhashCode : " + user2.hashCode())
println("user3のhashCode : " + user3.hashCode())
val userData1 = UserData("Foobar", 100)
val userData2 = UserData("Foobar", 100)
val userData3 = UserData("Barfoo", 0)
println("userData1のhashCode : " + userData1.hashCode())
println("userData2のhashCode : " + userData2.hashCode())
println("userData3のhashCode : " + userData3.hashCode())
}
Output
user1のhashCode : 1581781576
user2のhashCode : 1725154839
user3のhashCode : 1670675563
userData1のhashCode : 984111191
userData2のhashCode : 984111191
userData3のhashCode : 1331158637
toString()
toString()の動作を確認するために次のコードを動作させてみます。
クラスは呼び出した階層の情報?が表示され、データクラスはプロパティ値が表示されます。
データクラスのほうがtoStringしたときに使い道がある情報かもしれませんね。
Sample Code
fun main(args: Array<String>) {
class User(val name: String, val age: Int)
data class UserData(val name: String, val age: Int)
val user1 = User("Foobar", 100)
println("user1のtoString : " + user1.toString())
val userData1 = UserData("Foobar", 100)
println("userData1のtoString : " + userData1.toString())
}
Output
user1のtoString : advanced.dataclass.ToStringSampleKt$main$User@5e481248
userData1のtoString : UserData(name=Foobar, age=100)
componentN()
componentN()の動作を確認するために次のコードを動作させてみます。
この関数はクラスでは利用できませんが、データクラスでは次のように動作します。
プロパティを宣言した順番にcomponent1,component2…と割りついていきます。
そのためプロパティを宣言する順番を変えると割付き方も変化します。
Sample Code
fun main(args: Array<String>) {
data class UserData(val name: String, val age: Int)
val userData1 = UserData("Foobar", 100)
println("userData1のcomponent1 : " + userData1.component1())
println("userData1のcomponent2 : " + userData1.component2())
data class SubUserData(val age: Int, val name: String)
val subUserData1 = SubUserData(100, "Foobar")
println("subUserDataのcomponent1 : " + subUserData1.component1())
println("subUserDataのcomponent2 : " + subUserData1.component2())
}
Output
userData1のcomponent1 : Foobar
userData1のcomponent2 : 100
subUserDataのcomponent1 : 100
subUserDataのcomponent2 : Foobar
copy()
copy()の動作を確認するために次のコードを動作させてみます。
この関数はクラスでは利用できませんが、データクラスでは次のように動作します。
データクラスをコピーすると同じ値を持つ別のインスタンスが生成されます。
もちろん別のインスタンスが生成されているので値を変更しても元のインスタンスには影響を与えません。
Sample Code
fun main(args: Array<String>) {
data class UserData(var name: String, var age: Int)
val original = UserData("Foobar", 100)
val copy = original.copy()
println("変更前")
println("original : " + original.toString())
println("copy : " + copy.toString())
copy.name = "Bar"
copy.age = 0
println("変更後")
println("original : " + original.toString())
println("copy : " + copy.toString())
}
Output
変更前
original : UserData(name=Foobar, age=100)
copy : UserData(name=Foobar, age=100)
変更後
original : UserData(name=Foobar, age=100)
copy : UserData(name=Bar, age=0)
といったようにクラスとデータクラスでは関数の実装が異なります。
クラスとデータクラスの違いを確認する
なぜクラスとデータクラスに違いが出るのか気になる人もいると思います。(私は気になる方です…)
ですのでクラスとデータクラスでこのような機能に違いが出るのか調べたので説明します。
なぜかと言うとデータクラスで定義するとコンパイル時にコンパイラが関数を実装してくれるからです。
じゃどのようにコンパイラがコンパイル時に関数を実装してくれているのかバイトコードを見て確認しましょう。
生成されるJavaコード
今回は次のUserというクラスとUserDataというデータクラスを作成してみました。
どちらもNameとAgeのプロパティを持つ単純なクラスになっています。
User
class User(val name: String, val age: Int)
UserData
data class UserData(val name: String, val age: Int)
これらのクラスをコンパイルすると次のバイトコード(Javaにでコンパイルしてもの)が出力されます。
見るとデータクラスには上で説明した関数の実装が追加されていることがわかります。
このように"data class 〜"と宣言するだけでコンパイラが関数を実装してくれているですね。
User.decompiled.java
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\b\n\u0002\b\u0006\u0018\u00002\u00020\u0001B\u0015\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0005¢\u0006\u0002\u0010\u0006R\u0011\u0010\u0004\u001a\u00020\u0005¢\u0006\b\n\u0000\u001a\u0004\b\u0007\u0010\bR\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\t\u0010\n¨\u0006\u000b"},
d2 = {"Ladvanced/dataclass/User;", "", "name", "", "age", "", "(Ljava/lang/String;I)V", "getAge", "()I", "getName", "()Ljava/lang/String;", "Kotlin"}
)
public final class User {
@NotNull
private final String name;
private final int age;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public User(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
this.age = age;
}
}
UserData.decompiled.java
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\b\n\u0002\b\t\n\u0002\u0010\u000b\n\u0002\b\u0004\b\u0086\b\u0018\u00002\u00020\u0001B\u0015\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0005¢\u0006\u0002\u0010\u0006J\t\u0010\u000b\u001a\u00020\u0003HÆ\u0003J\t\u0010\f\u001a\u00020\u0005HÆ\u0003J\u001d\u0010\r\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u0005HÆ\u0001J\u0013\u0010\u000e\u001a\u00020\u000f2\b\u0010\u0010\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\u0011\u001a\u00020\u0005HÖ\u0001J\t\u0010\u0012\u001a\u00020\u0003HÖ\u0001R\u0011\u0010\u0004\u001a\u00020\u0005¢\u0006\b\n\u0000\u001a\u0004\b\u0007\u0010\bR\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\t\u0010\n¨\u0006\u0013"},
d2 = {"Ladvanced/dataclass/UserData;", "", "name", "", "age", "", "(Ljava/lang/String;I)V", "getAge", "()I", "getName", "()Ljava/lang/String;", "component1", "component2", "copy", "equals", "", "other", "hashCode", "toString", "Kotlin"}
)
public final class UserData {
@NotNull
private final String name;
private final int age;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public UserData(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
this.age = age;
}
@NotNull
public final String component1() {
return this.name;
}
public final int component2() {
return this.age;
}
@NotNull
public final UserData copy(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
return new UserData(name, age);
}
// $FF: synthetic method
@NotNull
public static UserData copy$default(UserData var0, String var1, int var2, int var3, Object var4) {
if ((var3 & 1) != 0) {
var1 = var0.name;
}
if ((var3 & 2) != 0) {
var2 = var0.age;
}
return var0.copy(var1, var2);
}
@NotNull
public String toString() {
return "UserData(name=" + this.name + ", age=" + this.age + ")";
}
public int hashCode() {
String var10000 = this.name;
return (var10000 != null ? var10000.hashCode() : 0) * 31 + this.age;
}
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof UserData) {
UserData var2 = (UserData)var1;
if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
return true;
}
}
return false;
} else {
return true;
}
}
}
おわりに
次にざっくりクラスとデータクラスの違いをまとめます。
クラスとデータクラスの違いを理解して良いコードを書きましょう。
- クラスとデータクラスではequal(),hashCode(),toString()関数の実装が異なる。
- データクラスにはcomponentN()とcopy()関数が実装される。
- 関数の実装内容の違いは本記事の本文に記載されているので確認する