Start learning free

LESSON 161 OF 414 · SCALA ESSENTIALS

Case Classes in Scala: toString, equals and copy for Free

Lesson Read the full explanation

Most of the classes you will ever write do one humble job: hold data. A User is a name and an age; a Point is an x and a y. In Scala you can declare such a class in a single line — the fields live right in the parentheses, no body needed:

class Visitor(val name: String, val age: Int)

But a plain class only stores the data — it doesn't know how to show it or compare it. Every class inherits two defaults: toString prints the class name plus a memory address, and == asks "are these the very same object?" — not "do they hold the same values?".


The trap:

Sooner or later, every Scala beginner prints an object, or compares two objects built from identical values — and gets this:

val a = new Visitor("Ana", 25)
val b = new Visitor("Ana", 25)
println(a)       // expecting the data...
println(a == b)  // expecting true...
Visitor@1b6d3586
false

No name, no age — just Visitor@ and a hex address. And two visitors with the exact same data are "not equal", because the default equals compares identity (the address), not contents.


The fix — one keyword:

Mark the class with case and the compiler generates the useful versions for you, straight from the constructor parameters (which automatically become public val fields — no val keyword needed):

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

println(User("Ana", 25))                    // User(Ana,25) — no `new` needed
println(User("Ana", 25) == User("Ana", 25)) // true — compares values

You also get copy for free: it clones an object while changing only the fields you name — ana.copy(name = "Bob") keeps the age and swaps the name. Since the fields are immutable vals, this is the idiomatic way to get a "changed" version without touching the original.

And copy really is a case-class exclusive — call it on a plain class and the compiler stops you:

-- [E008] Not Found Error ------------------------------------------------------
val other = a.copy(name = "Bob")
              ^^^^^^
              value copy is not a member of Visitor

Assignment
Follow the Coding Tutorial to see the freebies in action — and the plain-class trap right next to them:

  • Declare case class User with name: String and age: Int.
  • Create ana = User("Ana", 25) and print her — the generated toString does the formatting.
  • Create twin from the same values and print whether ana == twin.
  • Declare a regular class Visitor(val name: String, val age: Int), build v1 and v2 from identical values with new, and print whether v1 == v2.
  • Make bob with ana.copy(name = "Bob") and print him.

The exact expected output is:

ana = User(Ana,25)
same = true
plain same = false
bob = User(Bob,25)

Hint
If your output looks like User@1b6d3586, you forgot the case keyword. To embed a comparison in a printed string, wrap it in braces: s"same = ${ana == twin}".


STEP 1 OF 11
    Ctrl + Enter to check

    SCALA · SCALA ESSENTIALS

    Case Classes in Scala, explained

    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 write val yourself. ana.name and ana.age work. In a plain class, a parameter without val stays private to the class.
    • A readable toString. Printing a user shows User(Ana,25): the class name and the field values.
    • Value-based equality. == compares the fields, so two users with equal data are equal. The generated hashCode agrees with it, so equal users also behave as one element in a Set.
    • A factory. You can write User("Ana", 25) with no new.
    • A copy method. It builds a new object, changing only the fields you name.
    • Pattern matching. A case class can be taken apart in a match, as in case 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.

    1. Declare the case class. case class User(name: String, age: Int). One line is enough, because a case class needs no body.
    2. Create and print Ana. val ana = User("Ana", 25), then println(s"ana = $ana"). The s in front of the string turns on interpolation: $ana inserts the value's toString, which prints User(Ana,25).
    3. Compare with a twin. Build twin from the same values and print s"same = ${ana == twin}". An expression, as opposed to a single name, needs braces after the $. The two separate objects have equal fields, so this prints true.
    4. Declare the plain contrast. class Visitor(val name: String, val age: Int) has the same fields but no case. Here the val keywords are required to make the fields public.
    5. Build two visitors. v1 and v2 both come from new Visitor("Ana", 25). A plain class has no generated factory, so new is required.
    6. Compare them. s"plain same = ${v1 == v2}" prints false: the default == asks whether both names refer to one object, and they don't.
    7. Copy with a change. val bob = ana.copy(name = "Bob") clones Ana and replaces only the name. The age is carried over, and ana is not modified. Print it with s"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, so ana.age = 26 is a compile error ("Reassignment to val age"). To get a changed version, use copy and keep the original.
    • copy belongs to case classes. On the plain Visitor, visitor.copy(name = "Bob") doesn't compile: the compiler reports value copy is not a member of Visitor.
    • The shape of the printed text. The generated toString separates fields with a comma and no space, so the output is User(Ana,25), not User(Ana, 25). The exact text matters when the lesson's tests compare output.
    • Equality is not identity. ana == twin is true, but the two are still different objects. The operator eq compares references, and ana eq twin is false.
    • Without val the field is hidden. In a plain class, class P(name: String) keeps name private. Reading p.name from outside fails with an error saying it can only be accessed from class P.
    • Shallow copying. copy reuses the field values, it doesn't clone them deeply. This is safe for strings and numbers, which can't change.

    Key takeaways

    • Use case class for classes that mainly hold data.
    • The compiler gives you toString, value-based equals and hashCode, a factory without new, copy and pattern matching.
    • A plain class prints as Name@hex and compares by identity.
    • Use copy(field = value) to get a changed version of an immutable object.
    Keep going with ScalaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free