Why case classes exist
Most classes you write hold data: a User is a name and an age, a Point is an x and a y. Scala declares such a class in one line, with the fields in the parentheses:
class Visitor(val name: String, val age: Int)
This plain class stores its data, but it doesn't know how to show it or compare it. Every class starts with two default behaviours: toString prints the class name followed by @ and a hexadecimal number, and == asks whether two references point to the very same object, not whether the contents match.
The trap with plain classes
Build two visitors from identical values, print one and compare them:
val a = new Visitor("Ana", 25)
val b = new Visitor("Ana", 25)
println(a)
println(a == b)
The first line prints Visitor@ followed by a hexadecimal number, which varies between runs, and the second prints false. There is no name and no age in the output, and two objects with exactly the same data count as different, because each new creates a separate object.
The fix: add the case keyword
Write case in front of class and the compiler generates the missing pieces from the constructor parameters:
case class User(name: String, age: Int)
What you get:
- Public fields. Each constructor parameter becomes a public
val, so you don't writevalyourself.ana.nameandana.agework. In a plain class, a parameter withoutvalstays private to the class. - A readable
toString. Printing a user showsUser(Ana,25): the class name and the field values. - Value-based equality.
==compares the fields, so two users with equal data are equal. The generatedhashCodeagrees with it, so equal users also behave as one element in aSet. - A factory. You can write
User("Ana", 25)with nonew. - A
copymethod. It builds a new object, changing only the fields you name. - Pattern matching. A case class can be taken apart in a
match, as incase User(n, a) => ....
The exercise, step by step
The exercise puts the case class and a plain class side by side and prints four lines.
- Declare the case class.
case class User(name: String, age: Int). One line is enough, because a case class needs no body. - Create and print Ana.
val ana = User("Ana", 25), thenprintln(s"ana = $ana"). Thesin front of the string turns on interpolation:$anainserts the value'stoString, which printsUser(Ana,25). - Compare with a twin. Build
twinfrom the same values and prints"same = ${ana == twin}". An expression, as opposed to a single name, needs braces after the$. The two separate objects have equal fields, so this printstrue. - Declare the plain contrast.
class Visitor(val name: String, val age: Int)has the same fields but nocase. Here thevalkeywords are required to make the fields public. - Build two visitors.
v1andv2both come fromnew Visitor("Ana", 25). A plain class has no generated factory, sonewis required. - Compare them.
s"plain same = ${v1 == v2}"printsfalse: the default==asks whether both names refer to one object, and they don't. - Copy with a change.
val bob = ana.copy(name = "Bob")clones Ana and replaces only the name. The age is carried over, andanais not modified. Print it withs"bob = $bob".
The solution in Scala
case class User(name: String, age: Int)
val ana = User("Ana", 25)
println(s"ana = $ana")
val twin = User("Ana", 25)
println(s"same = ${ana == twin}")
class Visitor(val name: String, val age: Int)
val v1 = new Visitor("Ana", 25)
val v2 = new Visitor("Ana", 25)
println(s"plain same = ${v1 == v2}")
val bob = ana.copy(name = "Bob")
println(s"bob = $bob")
The program prints:
ana = User(Ana,25)
same = true
plain same = false
bob = User(Bob,25)
Details and gotchas
- Immutable by default. The generated fields are
vals, soana.age = 26is a compile error ("Reassignment to val age"). To get a changed version, usecopyand keep the original. copybelongs to case classes. On the plainVisitor,visitor.copy(name = "Bob")doesn't compile: the compiler reportsvalue copy is not a member of Visitor.- The shape of the printed text. The generated
toStringseparates fields with a comma and no space, so the output isUser(Ana,25), notUser(Ana, 25). The exact text matters when the lesson's tests compare output. - Equality is not identity.
ana == twinis true, but the two are still different objects. The operatoreqcompares references, andana eq twinis false. - Without
valthe field is hidden. In a plain class,class P(name: String)keepsnameprivate. Readingp.namefrom outside fails with an error saying it can only be accessed from classP. - Shallow copying.
copyreuses the field values, it doesn't clone them deeply. This is safe for strings and numbers, which can't change.
Key takeaways
- Use
case classfor classes that mainly hold data. - The compiler gives you
toString, value-basedequalsandhashCode, a factory withoutnew,copyand pattern matching. - A plain class prints as
Name@hexand compares by identity. - Use
copy(field = value)to get a changed version of an immutable object.
