Case Class in Scala

Case Class in Scala

What is a case class?

Case classes are a special kind of class created using the keyword case. Case classes are excellent for data transfer objects and for representing immutable data. It is a type of class that is mostly utilised for data storage.

case class Person(name: String, age: Int)

The following beneficial features or conveniences are automatically added to our class when the Scala compiler discovers a case class:

  • It adds a factory method, apply() for creating new instances, so we don’t need to use the keyword new to instantiate a class.

  • Unless all arguments in the parameter list of a case class are declared as var, all arguments implicitly get a val prefix, and thus the val keyword is optional. To put it another way, by default, case classes automatically transform arguments to value fields (val fields), therefore the val keyword is not required to prefix them. If we require a variable field, we can still use the var keyword, but this is not what the case class was intended for.

  • The compiler automatically implements the following methods for the class:

    • apply
    • unapply
    • copy
    • equals
    • hashCode
    • toString
  • Every case class has a method named copy that allows us to easily create a same or a modified copy of the class’s instance.

  • A companion object is created automatically with the appropriate apply and unapply methods.

The methods generated by the Scala compiler for case classes aren’t special in any way, other than that they are automatically generated for us. By adding the methods and companion objects ourselves, we may avoid using case classes. Because it would take a lot of time and effort to write each of these methods appropriately for every data-storage class, case classes have the advantage of being more convenient.

Like a regular class, a case class can extend other classes, including trait and case classes. Case classes are Scala’s way of allowing pattern matching on objects without requiring a large amount of boilerplate code.

Let's explore the advantages of a case class in action:

1 scala> case class Person(name: String, age: Int)
2 defined case class Person
3
4 scala> val person = Person("John", 36)
5 val person: Person = Person(John,36)
6
7 scala> val otherPerson = person.copy(name = "Robert")
8 val otherPerson: Person = Person(Robert,36)
9
10 scala> val someOtherPerson = person.copy()
11 val someOtherPerson: Person = Person(John,36)
12 scala> println(person.equals(someOtherPerson))
13 true
14
15 scala> person == otherPerson
16 val res0: Boolean = false
17 
18 scala> person match {
19     |     case Person(x, 36) => s"$x is a younger person"
20     |     case Person(x, 50) => s"$x is a older person"
21     | }
22 val res1: String = John is a younger person
  • Line 1: Able to instantiate without a new operator because of the companion object's factory method, Person.apply().

  • Line 5: The auto-generated toString method prints the fields in our instance.

  • Line 7: The second instance (otherPerson) shares the same value for the second field, so we only need to specify a new value for the first field in the copy method.

  • Line 10: Copies of case classes result in strict equivalence. Hence, the equals statement in line 12 results in true.

Case class extends other class: If our case class had extended another class with its own fields but we hadn’t added the fields as case class parameters, the generated methods wouldn’t have been able to make use of them. Before using case classes, it's necessary to be aware of this important caution.

Copy vs. clone

The case class’s copy method lets us make a copy of an object. Remember that a copy method differs from a clone method in that a copy allows us to modify fields at any time while it is being copied.

case class Worker(name: String, department: String)

object CaseClassCopyMethod extends App {
  val john = Worker("John", "Sales")
  val mike = john.copy(name="Mike")
}

Abstract case class

When we declare an abstract case class, Scala won’t generate the apply method in the companion object, which makes sense as we can’t create an instance of an abstract class.

 abstract case class PositiveInt(value: Int)

Case object

We can also create case objects. Just like a regular object, a case object inherits all the features of a regular object. Note that a case object is serialisable by default, whereas a regular object is not.

 case object Fruit {
    val costPerKg = 10
}

We must extend the Serializable trait in order to make a case object serialisable.