What is a case class in Scala?
A quick answer to the question of the title of this post is: it allows us to use pattern matching when comparing its instances and it does not need the keyword new
in order be initialized. But why? How?
Defining a case class
One situation (not the only one) when case classes are useful is when we have multiple classes that extend a trait or an abstract class.
abstract class Player
case class Computer(mark: Symbol) extends Player
case class Human(mark: Symbol, messages: String) extends Player
// a case class needs a list of parameters, even if it is empty
case class SuperPlayer() extends Player
// a case object - more about it in the end of this post
case object SuperEspecialPlayer extends Player
When a case class is defined, the compiler automatically creates its companion object, that holds a factory method that initializes instances of that class. That is the reason why we do not need the new
keyword.
// (part of the) implicit companion object created by the compiler
// the object has other methods besides `apply`
object Computer {
def apply(mark: String) = new Computer(mark)
}
By the way, a companion object is an object has exactly the same name — and is defined in the same file — as a class. It shares methods and even private fields with the parent class.
There is no need to worry about it when writing case classes. It is a compiler’s job to do it.
Accessing class members
We can write a “regular” class like this:
class Foo(_number: Int) {
val number = _number
}
val f = new Foo(42)
f.number
// 42
Or using the easier way:
class Foo(val number: Int)
val f = new Foo(42)
f.number
// 42
With a case class, the argument is automatically defined as public val
, a member of the given case class:
// case class Computer(mark: Symbol) extends Player
val computerPlayer = Computer('x)
computerPlayer.mark
//-> 'x
Because it is defined as a val
, we cannot reassign it:
computerPlayer.mark = 'o
// error: reassignment to val
We can explicitly define the argument as a var
, but that is not exactly a good idea. A case class is an immutable class (like List
, for instance, that is also an immutable class). We can, however, make copies — which creates new objects.
val computer = Computer('x)
val anotherComputer = computer.copy(mark = 'o)
anotherComputer.mark
//-> 'o
Overriding a constructor
Because the arguments passed to a case class constructor are val
s, they can cause a conflict between the extended class and the case class.
In a “regular” class, we get an error when we do the following:
class Foo(number: Int) {
val number = number
// error: reassignment to val
}
What happens in here is that number
, in the constructor, is a val
, but it is not a member of the class Foo. That is why we cannot call it directly:
class Foo(number: Int)
val f = new Foo(2)
f.number
// error: value 'number' is not a member of Foo
Understanding this is helpful when we need to extend a class that has a constructor. For example:
// THIS WILL NOT WORK
abstract class Player(_mark: Symbol) {
val mark = _mark
}
case class Computer(mark: Symbol) extends Player(mark)
// error: overriding value 'mark' in class Player of type Symbol;
// value 'mark' needs `override' modifier
This happens because mark
is a member of the class Player
and, Computer
, by extending Player
, has access to it. When we say case class Computer(mark: Symbol)
we are saying that mark
is a member of Computer
, and, therefore, it should explicitly override the member of Player
.
Fixing this problem is just a matter of changing the name of the arguments passed to the constructor.
abstract class Player(val mark: Symbol)
case class Computer(renamedMark: Symbol) extends Player(renamedMark)
val comp = Computer('o)
com.mark
//-> 'o
Pattern Matching
One of the reasons why using case classes when they extend a trait or abstract class is that, when we define a case class, the compiler also automatically generates an equals
method. This allows us to compare objects by structure (instead of by reference).
val c1 = Computer('x)
val c2 = Computer('x)
c1 == c2
// true
We can also use pattern matching when dealing with case classes.
def currentPlayerMessage(player: Player): String = player match {
case Computer(mark) => "Computer is playing with " + mark
case Human(mark, _) => "Human is playing with " + mark
case _ => "Game over"
}
Case object vs object
In the first example of the case class use, there is a case object
case object SuperEspecialPlayer extends Player
There is not a lot of reason why we would rather use a case object instead of a “regular” object. But they are different from each other.
Some of the differences include pretty printing and the ability to cast it to be a Serializable
(just like case classes):
object Obj
case object CaseObj
val obj = Obj
val caseObj = CaseObj
// 1. pretty print
println(obj)
//-> $line1.$read$$iw$$iw$Obj$@1e12345
println(caseObj)
//-> CaseObj
// 2. serialization
obj.asInstanceOf[Serializable]
//-> error: cannot be cast to scala.Serializable
caseObj.asInstanceOf[Serializable]
//-> Serializable = CaseObj