概要
この記事はScalaのクラス(case)とケースクラス(case class)の違い、またはケースクラスを宣言によって、自動生成された便利なメソッドとそのメリットについてまとめておこうと思います。
その1:ケースクラス初期化にnew
が不要になる
class Task(description: String, complete: Boolean)
val task = new Task("Learn Scala", true)
case class Task(description: String, complete: Boolean)
val task = Task("Learn Scala", true)
ここで、クラスのインスタンスを初期化する時、new
をつけないとコンパイルエラーが発生しますが、ケースクラスには必要ではないです。なぜというと、ケースクラスを宣言によって、コンパニオンオブジェクト(Companion Object)を自動生成してくれます。
コンパニオンオブジェクトにプライマリコンストラクタ引数と対応するapply()
メソッドが定義されます。このapply()
メソッドがあるので、ケースクラス初期化にnew
が不要になります。
では、もしクラスにコンパニオンオブジェクトを宣言し、apply()
メソッドも定義すれば、クラスの初期化でもnew
が不要になれますか?試してみましょう。
class Task(description: String, complete: Boolean)
object Task {
def apply(description: String, complete: Boolean) =
new Task(description, complete)
}
val task = Task("Learn Scala", true)
このような、コンパニオンオブジェクトにapply()
を定義したら、new
を使わなくでもエラーなく初期化ができました。
おそらく、Task("Learn Scala", true)
を見ると、apply()
メソッドが呼び出されていないじゃないかと見えますが、実はScalaのオブジェクトでapply
という名前のメソッドが特別に扱われます。Task("Learn Scala", true)
はTask.apply("Learn Scala", true)
と解釈されます。
コンパニオンオブジェクト
簡潔に言うと、クラス名と同じ名前のオブジェクトはコンパニオンオブジェクトと呼ばれています。そして、コンパニオンオブジェクトは対応するクラスに対して特権的なアクセス権を持っています。例えば、private
をつけたら、クラス内だけではなく、そのクラスのコンパニオンオブジェクトからのアクセスも可能です。
その2:プライマリコンストラクタ引数をアクセスできる
まずはクラスのサンプルコードからみてみましょう。
class Task(val description: String, complete: Boolean)
val task = new Task("Learn Scala", true)
task.description //"Learn Scala"
task.complete //エラー: value complete is not a member of main.Task
complete
フィールドにアクセスしようとするとエラーが発生しました。
クラスのコンストラクタ引数をフィールドとしてアクセスできないので、引数の前にval
またはvar
をつけたらアクセス可能です。(val
の場合は変更不能、var
の場合は変更可能なフィールドになります。)
ケースクラスのコンストラクタ引数にはval
を付けたかのように扱われますので、値は変更不能となります。var
を明示的につけていれば変更可能になります。
case class Task(description: String, var complete: Boolean)
val task = Task("Learn Scala", true)
task.description //"Learn Scala"
task.complete //true
task.description = "Learn SCALA"
task.complete = false //エラー: reassignment to val
その3:便利なメソッドを自動生成してくれる
ケースクラスはクラスに対して、いくらか自動生成してくれるものがあります。
1. toString()
メソッド
クラス内各フィールドの内容を出力するメソッドが生成してくれたので、ケースクラスは綺麗に文字列で表現できます。
class Task(description: String, complete: Boolean)
val task = new Task("Learn Scala", true)
println(task) //Task@591f989e
case class Task(description: String, complete: Boolean)
val task = Task("Learn Scala", true)
println(task) //Task(Learn Scala,true)
2. copy()
メソッド
フィールド値をコピーして新しいインスタンスを生成するメソッドです。
コンストラクタと同様の引数を受け取り、同じクラスのインスタンスを返します。
一部や全部の引数は変更可能、足りない部分は元のインスタンスの値が使われます。
case class Task(description: String, complete: Boolean)
val task = Task("Learn Scala", true)
val task2 = task.copy(complete = false)
println(task2) //Task(Learn Scala,false)
3. equals()
、hashCode()
、canEqual()
メソッド
インスタンス間の同値比較が行えるようにequals()
、hashCode()
、canEqual()
が定義されます。そして、==
を使うと時にequals()
メソッドが定義されている場合、equals()
メソッドに処理を委譲します。
クラスは同値比較メソッドがないので、task == sameTask
に対して同一性が比較されます。なので、インスタンスtask
とインスタンスsameTask
が同じインスタンスオブジェクトかどうかを判断されました(false
)。
class Task(description: String, complete: Boolean)
val task = new Task("Learn Scala", true)
val sameTask = new Task("Learn Scala", true)
println(task == sameTask) //false
ケースクラスは同値比較メソッドが自動的に定義されますので、task == sameTask
に対して同値性が比較されます。インスタンスtask
とインスタンスsameTask
が全く同じ値を持つことで、true
を返しました。
case class Task(description: String, complete: Boolean)
val task = Task("Learn Scala", true)
val sameTask = Task("Learn Scala", true)
println(task == sameTask)
4. apply()
、unapply()
メソッド
apply()
は値を構築するために使われますので、ケースクラス初期化の時にnew
は不要になります。
unapply()
の使う目的としては値をマッチングして分解することでパターンマッチングができます。
case class Task(description: String, complete: Boolean)
val task = Task("Learn Scala", true)
def caseClassesPatternMatching(task: Task): Unit = {
task match {
case Task(_, true) => println("Task completed! :D")
case Task(_, false) => println("Task incomplete :C")
}
}
caseClassesPatternMatching(task) //Task completed! :D
5. tupled()
メソッド
前述のapply()
メソッドとunapply()
メソッドとは別に、ケースクラスのコンパニオンオブジェクトはtupled()
メソッドも自動的に定義します。tupled()
メソッドを使用すると、タプルからケースクラスインスタンスを作成することもできます。
case class Task(description: String, complete: Boolean)
val tuple = ("Learn Scala", true)
val task = (Task.apply _).tupled(tuple)
println(task) //Task(Learn Scala,true)
6. Productトレイトがミックスインされる
ケースクラスはProductトレイトを継承するでProductの各メソッドが呼び出せます。
- productArity: Int
: コンストラクタの引数(フィールド)の個数を返す
- productElement(n: Int): Any
: コンストラクタn引数の値を返す
- productIterator: Iterator[Any]
: コンストラクタの引数の値を返す
- productPrefix: String
: クラス名の文字列を返す
case class Task(description: String, complete: Boolean)
val task = Task("Learn Scala", true)
println(task.productArity) //2
println(task.productElement(0)) //Learn Scala
task.productIterator.foreach(println) //Learn Scala true
println(task.productPrefix) //Task
Productトレイトを継承することでパラメーター個数の上限は22という制限もあります。
7. Serializableトレイトがミックスインされる
ケースクラスはSerializableトレイトを継承することでインスタンスシリアル化できます。インスタンスをシリアル化し、ファイルとして保存することとネットワーク経由で送信することができます。
case class Task(description: String, complete: Boolean)
val task = new Task("Learn Scala",true)
// インスタンスをファイルとして保存する
val oos = new ObjectOutputStream(new FileOutputStream("/tmp/task"))
oos.writeObject(task)
oos.close
// 保存したファイルから読み込む
val ois = new ObjectInputStream(new FileInputStream("/tmp/task"))
val task = ois.readObject.asInstanceOf[Task]
ois.close
println(task) //Task("Learn Scala",true)
ケースクラス実体のイメージ
さて、上述のポイントに基づいて、ケースクラス同じように扱われられるクラスを定義するとしたら、イメージとして次のようになるでしょう。
// Val: コンストラクタ引数をフィールドとしてアクセスできる
// Product, Serializableを継承し、メソッドを使える
class Task(val description: String, val complete: Boolean) extends AnyRef with Product with Serializable {
// 同値比較メソッド
override def equals(that: Any): Boolean = that match {
case thatTask: Task =>
thatTask.canEqual(this) && this.description == thatTask.description && this.complete == thatTask.complete
case _ =>
false
}
override def hashCode(): Int = description.hashCode ^ complete.hashCode
def canEqual(that: Any): Boolean = that.isInstanceOf[Task]
// 文字列で表現できる
override def toString(): String = "Task(" + description + ", " + complete + ")"
// フィールド値をコピーできる
def copy(description: String = description, complete: Boolean = complete) =
Task(description, complete)
// Productの各メソッド
override def productPrefix: String = "Task"
def productArity: Int = 2
def productElement(n: Int): Any = n match {
case 0 => description
case 1 => complete
}
override def productIterator: Iterator[Any] = scala.runtime.ScalaRunTime.typedProductIterator(this)
}
object Task {
// 初期化`new`が不要、タプルできる
def apply(description: String, complete: Boolean): Task = new Task(description, complete)
// パターンマッチングができる
def unapply(t: Task): Option[(String, Boolean)] = Some((t.description, t.complete))
}
まとめ
ケースクラス(case class)によって自動生成されるものを以下となります。
- パラメーターフィールドとしてアクセス可能(
val
のように扱われる)。 -
toString()
が定義されたので、インスタンスを文字列で表現可能。 -
copy()
が定義されたので、値をコピーして新しいインスタンスを生成可能。 -
equals()
、hashCode()
、canEqual()
が定義されたので、インスタンス間の同値比較可能。 -
apply()
が定義されたので、new
なしでインスタンスを作成可能。 -
apply()
とtupled()
が定義されたので、タプルからインスタンスを作成可能。 -
unapply()
が定義されたので、パターンマッチング可能。 - Productトレイトを継承したので、Productの各メソッドを使用可能。
- Serializableトレイトを継承したので、でインスタンスシリアル化可能。
ここまでお読みいただきありがとうございました。
もし文法ミスや誤字がありましたら、編集リクエストでご意見いただければと思います。