Scala Tutorial: Case Classes

In Scala, a case class is a regular class with extra functionality for representing immutable data structures. In this tutorial, we'll discuss the advantage of using case classes and provide examples of how to create and use case classes in Scala.

What is a Case Class?

A case class is a regular class with some extra functionality. Specifically, a case class exports it's constructor parameters as immutable, publicly available properties and has built in methods that reduce the need for boilerplate code. Case classes are decomposable through pattern matching and immutable by default as they exclusively depend on their constructor arguments.

Case Class vs Regular Class

To illustrate the key differences between a case class and a regular class, let's look at quick example of implementing both:

class User(var name:String, var age:Int)
case class CaseUser(name:String, age:Int)
object Demo {
  def main(args: Array[String]) {
    val user1 = new User("Joe", 30)
    val user2 = new User("Joe", 30)
    val caseUser1 = CaseUser("Joe", 30)
    val caseUser2 = CaseUser("Joe", 30)
    println(user1 == user2) //false
    println(caseUser1 == caseUser2) //true
  }
}

In the example above, notice how we create a regular class User and a case class CaseUser.

Constructor Params

While both the User class and CaseUser class take two constructor params name and age, the case class will automatically create immutable "read only" properties whereas the regular class specifies var or val with the constructor params.

The "new" keyword

When we create a new instance of the User class, we must use new. With a case class, we can omit the new because of a built in factory method apply that takes care of this for us.

Equality

When we compare equality of a regular class user1 == user2 we get false because it's comparing the instance itself. While user1 and user2 have the same properties, they are not equal because they are separate instances of the User class.

The case class implements it's own method for checking equality and compares instances based on their constructor params rather than the instance itself. This is why caseUser1 == caseUser2 is true becuase both caseUser1 and caseUser2 have the same properties.

Scala Case Class Methods

In the example above, we saw that the case class implements it's own methods for checking equality and creating new instances (hence why we can omit the new keyword when creating an instance of a case class). The case class automatically generates several methods for you, including some that override their standard implementation and others that provide extra functionality, including:

toString: overrides regular implementation to return the value of an instance instead of it's memory address

equals: overrides regular implementation to compare equality based on structure instead of reference

hashCode: overrides regular implementation according to equality

copy: method for copying a case class with potentially modified values

Why Use a Case Class?

So why use a case class? You've already seen how the case class eliminates a great deal of boilerplate code for you. It automatically includes some methods for you. Additionally, constructor parameters automatically become immutable properties. This makes the case class great for representing immutable data structures in a clear, concise pattern. Especially when dealing with concurrency, the case class guarantees immutability with your data structures.

Pattern Matching

Pattern matching is a great way to deconstruct a certain value into it's constituent parts. Case classes inherently work with pattern matching. Take the following example:

case class CaseUser(name:String, age:Int)
object Demo {
  def main(args: Array[String]) {
    val user = CaseUser("Joe", 30)
    user match {
      case CaseUser(name, age) => println(s"Hello, my name is $name.  I am $age years old")
    }
  }
}

In the example above, notice how we create a case class CaseUser. We then use pattern matching with our user instance to deconstruct the class properties. This is not something inherently supported with regular classes and provides a great alternative to using an if/else or switch statement to deconstruct or abstract values from our instance.

Conclusion

The case class is great for representing immutable data containers. A case class ensures immutability and provides syntactic sugar by eliminating boilerplate code and providing methods for you. By exporting constructor arguments to immutable read only properties, case classes provide a clear and concise pattern for working with immutable data types in Scala.

Your thoughts?