A Quick Review of Scala’s Case Classes

“The biggest advantage of case classes is that they support pattern matching.”

Programming in Scala (Odersky, Spoon, and Venners)

Goals

In this book I generally assume that you know the basics of the Scala programming language, but because case classes are so important to functional programming in Scala it’s worth a quick review of what case classes are — the features they provide, and the benefits of those features.

Discussion

As opposed to a “regular” Scala class, a case class generates a lot of code for you, with the following benefits:

  • An apply method is generated, so you don’t need to use the new keyword to create a new instance of the class.
  • Accessor methods are generated for each constructor parameter, because case class constructor parameters are public val fields by default.
  • (You won’t use var fields in this book, but if you did, mutator methods would also be generated for constructor parameters declared as var.)
  • An unapply method is generated, which makes it easy to use case classes in match expressions. This is huge for Scala/FP.
  • As you’ll see in the next lesson, a copy method is generated. I never use this in Scala/OOP code, you’ll use it all the time in Scala/FP.
  • equals and hashCode methods are generated, which lets you compare objects and easily use them as keys in maps (and sets).
  • A default toString method is generated, which is helpful for debugging.

A quick demo

To demonstrate how case classes work, here are a few examples that show each of these features and benefits in action.

No need for new

When you define a class as a case class, you don’t have to use the new keyword to create a new instance:

scala> case class Person(name: String, relation: String)
defined class Person

// "new" not needed before Person
scala> val christina = Person("Christina", "niece")
christina: Person = Person(Christina,niece)

This is a nice convenience when writing Scala/OOP code, but it’s a terrific feature when writing Scala/FP code, as you’ll see throughout this book.

No mutator methods

Case class constructor parameters are val by default, so an accessor method is generated for each parameter, but mutator methods are not generated:

scala> christina.name
res0: String = Christina

// can't mutate the `name` field
scala> christina.name = "Fred"
<console>:10: error: reassignment to val
       christina.name = "Fred"
                  ^

unapply method

Because an unapply method is automatically created for a case class, it works well when you need to extract information in match expressions, as shown here:

scala> christina match { case Person(n, r) => println(n, r) }
(Christina,niece)

Conversely, if you try to use a regular Scala class in a match expression like this, you’ll quickly see that it won’t compile.

You’ll see many more uses of case classes with match expressions in this book because pattern macthing is a BIG feature of Scala/FP.

A class that defines an unapply method is called an extractor, and unapply methods enable match/case expressions. (I write more on this later in this book.)

copy method

A case class also has a built-in copy method that is extremely helpful when you need to clone an object and change one or more of the fields during the cloning process:

scala> case class BaseballTeam(name: String, lastWorldSeriesWin: Int)
defined class BaseballTeam

scala> val cubs1908 = BaseballTeam("Chicago Cubs", 1908)
cubs1908: BaseballTeam = BaseballTeam(Chicago Cubs,1908)

scala> val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016)
cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016)

I refer to this process as “update as you copy,” and this is such a big Scala/FP feature that I cover it in depth in the next lesson.

equals and hashCode methods

Case classes also have generated equals and hashCode methods, so instances can be compared:

scala> val hannah = Person("Hannah", "niece")
hannah: Person = Person(Hannah,niece)

scala> christina == hannah
res1: Boolean = false

These methods also let you easily use your objects in collections like sets and maps.

toString methods

Finally, case classes also have a good default toString method implementation, which at the very least is helpful when debugging code:

scala> christina
res0: Person = Person(Christina,niece)

Looking at the code generated by case classes

You can see the code that Scala case classes generate for you. To do this, first compile a simple case class, then disassemble the resulting .class files with javap.

For example, put this code in a file named Person.scala:

// note the `var` qualifiers
case class Person(var name: String, var age: Int)

Then compile it:

$ scalac Person.scala

scalac creates two JVM class files, Person.class and Person$.class. Disassemble Person.class with this command:

$ javap Person

With a few comments that I added, this command results in the following output, which is the public signature of the class:

Compiled from "Person.scala"
public class Person extends java.lang.Object implements scala.ScalaObject,scala.Product,scala.Serializable{
    public static final scala.Function1 tupled();
    public static final scala.Function1 curry();
    public static final scala.Function1 curried();
    public scala.collection.Iterator productIterator();
    public scala.collection.Iterator productElements();
    public java.lang.String name();           # getter
    public void name_$eq(java.lang.String);   # setter
    public int age();                         # getter
    public void age_$eq(int);                 # setter
    public Person copy(java.lang.String, int);
    public int copy$default$2();
    public java.lang.String copy$default$1();
    public int hashCode();
    public java.lang.String toString();
    public boolean equals(java.lang.Object);
    public java.lang.String productPrefix();
    public int productArity();
    public java.lang.Object productElement(int);
    public boolean canEqual(java.lang.Object);
    public Person(java.lang.String, int);
}

Next, disassemble Person$.class:

$ javap Person$

Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction2 ↵
implements scala.ScalaObject,scala.Serializable{
    public static final Person$ MODULE$;
    public static {};
    public final java.lang.String toString();
    public scala.Option unapply(Person);
    public Person apply(java.lang.String, int);
    public java.lang.Object readResolve();
    public java.lang.Object apply(java.lang.Object, java.lang.Object);
}

As javap shows, Scala generates a lot of source code when you declare a class as a case class, including getter and setter methods, and the methods I mentioned: copy, hashCode, equals, toString, unapply, apply, and many more.

As you see, case classes have even more methods, including tupled, curry, curried, etc. I discuss these other methods in this book as the need arises.

Case class compared to a “regular” class

As a point of comparison, if you remove the keyword case from that code — making it a “regular” Scala class — then compile it and disassemble it, you’ll see that Scala only generates the following code:

public class Person extends java.lang.Object{
    public java.lang.String name();
    public void name_$eq(java.lang.String);
    public int age();
    public void age_$eq(int);
    public Person(java.lang.String, int);
}

As you can see, that’s a BIG difference. The case class results in 22 more methods than the “regular” class. In Scala/OOP those extra fields are a nice convenience, but as you’ll see in this book, these methods enable many essential FP features in Scala.

Summary

In this lesson I showed that the following methods are automatically created when you declare a class as a case class:

  • apply
  • unapply
  • accessor methods are created for each constructor parameter
  • copy
  • equals and hashCode
  • toString

These built-in methods make case classes easier to use in a functional programming style.

What’s next

I thought it was worth this quick review of Scala case classes because the next thing we’re going to do is dive into the case class copy method. Because you don’t mutate objects in FP, you need to do something else to create updated instances of objects when things change, and the way you do this in Scala/FP is with the copy method.

See also