Classes and case classes
A class with a constructor and its own toString, then the same record as a case class, where copy, value equality and pattern matching come for free.
A class is a recipe for a value
A class groups a few values together with the work that belongs to them. class Book(val title: String, val pages: Int) puts both fields in the constructor, so new Book("Scala", 300) builds one and b.title reads a field back. The val in front of each parameter is what makes it a field; leave it out and the name exists only while the constructor runs.
Methods go inside the braces: def describe: String = .... toString is the method printing uses, so overriding it decides what println(b) shows, and the usual shape is the class name with the field values in brackets. A plain class prints as Book(Scala) only because that line was written by hand.
case class writes the rest
A case class Item(name: String, qty: Int) declares the same two fields and gets a great deal for free: a toString that reads Item(lamp,2), a sensible equals, and a copy method. No new is needed at the call site either — Item("lamp", 2) builds one directly.
copy returns a changed duplicate: a.copy(qty = 5) gives a new Item with the same name and the new quantity, and the original is left exactly as it was. Equality changes too. Two case class values with the same fields are equal, which is what a record should do and what a plain class does not do without an equals of its own.
Taking a case class apart
A case class can be matched by its fields: case Item(n, q) => ... binds n and q to the two values. Add a guard — case Item(n, q) if q > 1 — and the same pattern fits only the rows you care about, with a second case catching the rest.
That combination is why case classes are the default choice for data in Scala. The type holds the record, copy edits it, and match reads it back without a row of getters. A page of business rules then becomes a list of shapes you can see, rather than a pile of checks on fields that might be null.
What the constructor leaves behind
new Book("Scala", 300) does three things in order: it reserves an object, runs the constructor body, and stores the parameters marked with val as fields. A parameter without val is not a field: it can be used inside the constructor body, but book.title will not compile. The fields are what every later method and every reader can reach.
A case class writes a companion object beside the class. The companion has an apply method, which is why Item("lamp", 2) builds a value without new, and an unapply method, which is what lets case Item(n, q) take the record apart. The class itself gets copy, a field-by-field equals, a matching hashCode, and a readable toString.
Equality is the part that changes behaviour. A plain class inherits reference equality, so two separately built objects with the same fields are different. A case class compares its fields, so two records with the same values are equal and can be used as map keys. copy is shallow: it builds a new record with the named changes and keeps every other field as it was.
Methods that use the fields
A method inside a class reads the fields directly, without a prefix: def summary: String = title + " (" + pages + " pages)". The fields are in scope for every method of that class, which is what makes the class more than a bag of values — the arithmetic and the wording live with the data they describe.
A method that takes no arguments is written without brackets, so b.summary reads like a field. That is a convention rather than a rule, but it is the one library code follows.
Three common slips: leaving val off a constructor parameter and then wondering why the field is missing, trying a.qty = 5 on a case class (the field is a val, so use a.copy(qty = 5)), and comparing two plain class instances with == when only their fields match. The first two are compile errors; the third quietly answers false.
Try these next
- Add a
pagesfield toItemand repair every construction and pattern that mentions it. - Build two plain
Books with the same fields and compare them, then do the same with a case class. - Copy a record twice, changing a different field each time, and print all three values.
The same record, written twice
class Book(val title: String, val pages: Int) {
def describe: String = title + " (" + pages + ")"
override def toString: String = "Book(" + title + ")"
}
val b = new Book("Scala", 300)
println(b.describe) // Scala (300)
println(b) // Book(Scala)
case class Item(name: String, qty: Int)
val a = Item("lamp", 2)
println(a) // Item(lamp,2)
println(a.copy(qty = 5)) // Item(lamp,5)
a match {
case Item(n, q) if q > 1 => println(n + " x" + q)
case Item(n, _) => println(n)
}Stock records
Run it and compare the plain class with the case class line by line. Then add a third field to `Item`, repair the `copy` call, and see what the pattern match wants from you.
A library record
Run it and compare the original book with the copied one. Then change the year in the copy, add a second book, and match on both to print a short shelf note.
A case class field is a val already, and the companion apply builds the object: write Item("lamp", 2), not new Item("lamp", 2). Both compile, but the short form is the one every library expects to read.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
In class Book(val title: String), what does the val do?
