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

# 概要
この記事はScalaのクラス(case)とケースクラス(case class)の違い、またはケースクラスを宣言によって、自動生成された便利なメソッドとそのメリットについてまとめておこうと思います。

# その１：ケースクラス初期化に`new`が不要になる

```scala
class Task(description: String, complete: Boolean)
val task = new Task("Learn Scala", true)
```
```scala
case class Task(description: String, complete: Boolean)
val task = Task("Learn Scala", true)
```
ここで、クラスのインスタンスを初期化する時、`new`をつけないとコンパイルエラーが発生しますが、ケースクラスには必要ではないです。なぜというと、ケースクラスを宣言によって、コンパニオンオブジェクト(Companion Object)を自動生成してくれます。

コンパニオンオブジェクトにプライマリコンストラクタ引数と対応する`apply()`メソッドが定義されます。この`apply()`メソッドがあるので、ケースクラス初期化に`new`が不要になります。

では、もしクラスにコンパニオンオブジェクトを宣言し、`apply()`メソッドも定義すれば、クラスの初期化でも`new`が不要になれますか？試してみましょう。

```scala
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`をつけたら、クラス内だけではなく、そのクラスのコンパニオンオブジェクトからのアクセスも可能です。

---

# その２：プライマリコンストラクタ引数をアクセスできる

まずはクラスのサンプルコードからみてみましょう。

```scala
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`を明示的につけていれば変更可能になります。

```scala
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
```

# その３：便利なメソッドを自動生成してくれる
ケースクラスはクラスに対して、いくらか自動生成してくれるものがあります。

## 1. `toString()`メソッド
クラス内各フィールドの内容を出力するメソッドが生成してくれたので、ケースクラスは綺麗に文字列で表現できます。

```scala
class Task(description: String, complete: Boolean)
val task = new Task("Learn Scala", true)
println(task)  //Task@591f989e
```
```scala
case class Task(description: String, complete: Boolean)
val task = Task("Learn Scala", true)
println(task)  //Task(Learn Scala,true)
```

## 2. `copy()`メソッド
フィールド値をコピーして新しいインスタンスを生成するメソッドです。
コンストラクタと同様の引数を受け取り、同じクラスのインスタンスを返します。
一部や全部の引数は変更可能、足りない部分は元のインスタンスの値が使われます。

```scala
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`)。

```scala
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`を返しました。

```scala
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()`の使う目的としては値をマッチングして分解することでパターンマッチングができます。

```scala
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()`メソッドを使用すると、タプルからケースクラスインスタンスを作成することもできます。

```scala
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`: クラス名の文字列を返す

```scala
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トレイトを継承することでインスタンスシリアル化できます。インスタンスをシリアル化し、ファイルとして保存することとネットワーク経由で送信することができます。

```scala
  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)
```

# ケースクラス実体のイメージ

さて、上述のポイントに基づいて、ケースクラス同じように扱われられるクラスを定義するとしたら、イメージとして次のようになるでしょう。

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

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