9
1

More than 3 years have passed since last update.

【Scala】クラス(class)とケースクラス(case class)の違いについてまとめ

Last updated at Posted at 2020-11-17

概要

この記事は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トレイトを継承したので、でインスタンスシリアル化可能。

ここまでお読みいただきありがとうございました。
もし文法ミスや誤字がありましたら、編集リクエストでご意見いただければと思います。

9
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
1