1. Overview
Modern object-oriented programming languages very often provide abstractions to represent simple, immutable data. We have such an abstraction in Scala, and it’s called case class.
In this tutorial, we’ll learn about case classes in Scala, how they differ from regular classes, and what benefits they give.
2. Case Class
Let’s start by defining an example case class:
case class CovidCountryStats(countryCode: String, deaths: Int, confirmedCases: Int)
By default, all constructor parameters are public and immutable. We can change them to be var, but it’s not what they were designed for.
We can notice here a difference from a usual class, where constructor parameters are private by default. The idea of Scala’s case classes might look similar to Kotlin’s data classes, and rightly so! Also, Java 14 introduced similar concept called records.
Now, let’s have a tour of different features of case classes.
2.1. Short Initialization Syntax
We can create an instance of a case class in a short and concise way:
val covidPL = CovidCountryStats("PL", 776, 15366)
Please note, we’ve skipped the new keyword. Such a possibility exists because case classes have an apply function auto-generated by the compiler.
Now, let’s imagine having case classes nested in case classes. We can instantiate complex objects in a readable way!
2.2. Pattern Matching
Pattern matching allows us to nicely decompose classes and write intuitive code. It’s definitely the coolest feature we have in case classes:
covidPL matches {
case CovidCountryStats("PL", x, y) => println("Death rate for Poland is " + x.toFloat / y.toFloat)
...
case _ => println("Unknown country")
}
Such a construct is possible because case classes have a companion object generated by the compiler by default. The companion object contains the apply method we’ve mentioned before and an unapply extractor method that allows a class to be pattern-matched.
2.3. Auto-Generated Methods
Similarly to the data class in Kotlin, Scala’s case class has automatically defined hashcode and equals methods. Also, we have all getters defined by default.
It’s worth mentioning that case classes also provide decent, default toString method implementations.
2.4. Equality
In contrast to usual classes, case class instances are compared by their structure instead of their references. The code below will produce true:
assert(CovidCountryStats("PL", 776, 15366) == CovidCountryStats("PL", 776, 15366))
If we’d removed the case keyword from the definition of CovidCountryStats, the result would’ve been false.
2.5. Product
Since case classes extend the Product trait by default, they inherit several methods:
- productElement(n: Int): Any, which returns the n-th parameter of a class
- productArity: Int, which returns a number of parameters
- productIterator: Iterator[Any], which allows iterating over the parameters
Here comes one limitation — because they extend the Product trait, case classes can’t have more than 22 parameters.
2.7. Copying
Case classes have a copy method generated by default:
val covidUA = covidPL.copy(countryCode = "UA")
It’s important to note that copy methods guarantee shallow copies only.
2.8. Tupled
Apart from the aforementioned apply and unapply methods, a case class’s companion object defines the tupled method. The tupled method allows us to create a case class object from a tuple:
val tuple = ("PL", 776, 15366)
val covidPL = (CovidCountryStats.apply _).tupled(tuple)
2.9. Additional Considerations
There’s another limitation we need to know when designing our code with case classes: a case class can’t inherit from another case class.
It’s also worth mentioning that case classes implement Serializable.
3. Conclusion
In this article, we’ve taken a closer look at case classes in Scala.
We’ve described the benefits they give and compared them to regular classes. As we can see, they can serve pretty well as simple data containers due to the significant reduction of boiler-plate code.
However, we must be aware that the cost of having such a clean and neat code is an additional 20 methods generated by the compiler, which we most likely won’t ever want to use.
After all, case class features are just syntactic sugar. A nice exercise might be to use the scalac -print command to see what exactly happens under-the-hood.
As usual, code examples are available over on GitHub.