Kotlinのdata classと普通のクラスの違いについて

採用はこちら

Kotlinでは、data classと普通のclassは見た目こそ似ていますが、役割がはっきり異なります。

簡単にいうと、data classデータを表すためのクラスで、普通のclassより自由に設計できる一般的なクラスです。

この違いは、単なる書き方の差ではありません。

data classには、値を扱いやすくするための便利な機能が最初から備わっています。

一方、普通のclassは、必要に応じて自分で設計していく前提のクラスです。

目次

data classとは

data classは、名前の通りデータを持つことを主目的にしたクラスです。

たとえば、次のようなコードです。

data class User(val name: String, val age: Int)

このクラスは、単にnameageという値を持つだけでなく、Kotlinのコンパイラによって、値オブジェクトとして便利に使える仕組みが自動的に用意されます。

代表的なのは次の機能です。

  • equals()
  • hashCode()
  • toString()
  • copy()
  • componentN()

つまり、data classは「ただのクラス」ではなく、値を扱いやすくするための特別なクラスと考えるとわかりやすいです。

普通のclassとは

一方、普通のclassは、Kotlinにおける一般的なクラスです。

class User(val name: String, val age: Int)

この形でもインスタンスは作れますし、プロパティも持てます。

ただし、data classのように値比較やコピー機能などが自動で整うわけではありません。

そのため、普通のclassは、

  • ロジックを持たせたい
  • 状態を管理したい
  • 振る舞いを中心に設計したい
  • クラスの意味や責務を明確に持たせたい

といった場面に向いています。

いちばん大きな違いは「値として扱いやすいかどうか」

data classと普通のclassの差を理解するうえで、まず押さえたいのが比較のされ方です。

data classは「中身」で比較しやすい

data classでは、プライマリコンストラクタに定義したプロパティをもとに、equals()hashCode()が自動生成されます。

そのため、次のように中身が同じなら等しいと判断できます。

data class User(val name: String, val age: Int)

val u1 = User("Taro", 20)
val u2 = User("Taro", 20)

println(u1 == u2)   // true
println(u1 === u2)  // false

ここで大事なのは、Kotlinには=====の2種類の比較があることです。

  • == は中身の比較
  • === は同じインスタンスかどうかの比較

この例では、u1u2は別々に作られたインスタンスなので===falseです。

しかし、data classでは値をもとにequals()が作られるので、==trueになります。

普通のclassは、何もしないと別物として扱われやすい

普通のclassでも==equals()を使います。

ただし、自分でequals()をオーバーライドしていない場合は、デフォルトの実装が使われます。

そのため、同じ値を持っていても、通常は別インスタンスなら等しくなりません。

class User(val name: String, val age: Int)

val u1 = User("Taro", 20)
val u2 = User("Taro", 20)

println(u1 == u2)   // false
println(u1 === u2)  // false

ここが、data classと普通のclassの非常に大きな違いです。

toString()の違い

data classは、ログ出力やデバッグでも便利です。

data class User(val name: String, val age: Int)

println(User("Taro", 20))

出力は次のようになります。

User(name=Taro, age=20)

このように、プロパティ名と値が見やすく表示されます。

一方、普通のclassにもtoString()自体はありますが、data classのように中身がわかりやすい形式で自動表示されるとは限りません。

そのため、確認しやすさやデバッグのしやすさではdata classが有利です。

copy()が使える

data classの大きな特徴のひとつが、copy()を使えることです。

data class User(val name: String, val age: Int)

val u1 = User("Taro", 20)
val u2 = u1.copy(age = 21)

println(u1) // User(name=Taro, age=20)
println(u2) // User(name=Taro, age=21)

このように、元のオブジェクトを残したまま、一部だけ変更した新しいオブジェクトを簡単に作れます。

これは特に、

  • UIの状態管理
  • 不変オブジェクトの設計
  • APIレスポンスの加工
  • 設定値の更新

などで非常に便利です。

ただし、copy()浅いコピーです。

プロパティの中にリストやミュータブルなオブジェクトがある場合、その中身までは複製されません。

この点は注意が必要です。

分割代入ができる

data classでは、分割代入もできます。

data class User(val name: String, val age: Int)

val user = User("Taro", 20)
val (name, age) = user

このように、オブジェクトから値を取り出しやすいのも特徴です。

普通のclassでは、こうした機能は自動では使えません。

比較対象になるのはプライマリコンストラクタのプロパティだけ

ここは非常に重要です。

data classで自動生成されるequals()hashCode()toString()などの対象になるのは、プライマリコンストラクタに定義されたプロパティだけです。

たとえば次のコードを見てください。

data class User(val name: String) {
    var age: Int = 0
}

val u1 = User("Taro").also { it.age = 20 }
val u2 = User("Taro").also { it.age = 99 }

println(u1 == u2) // true

この場合、ageはクラス本体で定義されたプロパティなので、equals()の比較対象に入りません。

そのため、nameが同じである以上、u1 == u2trueになります。

つまり、data classを使うときは、何を値比較の対象にしたいのかを意識して設計する必要があります。

data classには制約がある

data classは便利ですが、何にでも使えるわけではありません。

主な制約は次の通りです。

  • プライマリコンストラクタに少なくとも1つの引数が必要
  • その引数はvalまたはvarである必要がある
  • abstractopensealedinnerにはできない

つまり、data classは自由度の高い万能クラスではなく、値を表す用途に特化した仕組みです。

継承との関係

普通のclassは、必要に応じて継承を前提にした設計ができます。

ただしKotlinでは、クラスはデフォルトで継承不可なので、継承させたい場合はopenを付けます。

open class Animal
class Dog : Animal()

一方で、data classopenにできません。

そのため、他のクラスに継承される前提の基底クラスには向いていません。

ただし、data class自身がインターフェースを実装したり、別のクラスを継承したりできる場合はあります。

要するに、「継承される土台」には向かないと理解するとわかりやすいです。

data classはどんなときに使うべきか

data classは、次のような用途にとても向いています。

  • APIレスポンス
  • DTO
  • 設定値
  • フォーム入力値
  • UIの状態
  • パラメータのまとまり
  • 検索条件

たとえば、画面状態を表すクラスは典型例です。

data class LoginUiState(
    val isLoading: Boolean = false,
    val email: String = "",
    val password: String = "",
    val errorMessage: String? = null
)

このようなクラスは、「振る舞い」よりも「値のまとまり」であることが本質なので、data classと非常に相性がいいです。

普通のclassはどんなときに使うべきか

普通のclassが向いているのは、責務や振る舞いが中心のオブジェクトです。

たとえば、次のようなものです。

class BankAccount(
    private var balance: Int
) {
    fun deposit(amount: Int) {
        require(amount > 0)
        balance += amount
    }

    fun withdraw(amount: Int) {
        require(amount > 0)
        require(balance >= amount)
        balance -= amount
    }

    fun currentBalance(): Int = balance
}

このクラスの本質は、「値の集まり」ではなく、「口座としてのルールを持つこと」です。

こういうものは、data classより普通のclassのほうが自然です。

実務での使い分け

迷ったときは、次のように考えると判断しやすいです。

data classが向いているケース

  • 主目的がデータ保持
  • 同じ値なら同じものとして扱いたい
  • 一部だけ変えた新しいオブジェクトを作りたい
  • ログやデバッグを見やすくしたい

普通のclassが向いているケース

  • ロジックや責務が中心
  • 内部状態を厳密に管理したい
  • 継承や抽象化を使いたい
  • copy()で簡単に複製されると困る

まとめ

Kotlinのdata classと普通のclassの違いは、単なる文法の差ではなく、そのクラスを何として扱いたいかの違いです。

data classは、値を持ち、値として比較され、コピーや出力がしやすい、データ中心のクラスです。

一方、普通のclassは、責務や振る舞いを持たせやすい、汎用的なクラスです。

いちばん実践的に覚えるなら、次の2つで十分です。

  • data class = 値のまとまり
  • 普通のclass = 振る舞いを持つオブジェクト

この基準で考えると、かなり迷いにくくなります。

以上、Kotlinのdata classと普通のクラスの違いについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次