Published 2014-08-01.
Last modified 2016-07-04.
Time to read: 5 minutes.
Scala case classes are terrific for creating domain models, pattern matching, and many other purposes. This lecture shows how to work with them.
The sample code for this lecture can be found in
courseNotes/
.
Case classes are great for building domain models and pattern matching. We won’t explore those uses in this lecture – for now, we’ll just take our first look at case classes. The Sealed Classes and Extractors lecture will discuss how to create case classes in a way that guarantees initialization logic is applied whenever a case class is constructed.
What is a Case Class?
Each case class is merely a regular class with some standard methods defined,
plus an automatically defined companion object with standard methods defined.
In addition, each of the class’s constructor parameters are also automatically defined as an immutable property,
just as if the val
keyword had prefixed each parameter.
To define a case class, merely preface the class
keyword with case
.
Let’s use the REPL to experiment with case classes.
scala> case class Frog9a(canSwim: Boolean, numLegs: Int, breathesAir: Boolean) defined class Frog9a
This is equivalent to:
case class Frog9b(val canSwim: Boolean, val numLegs: Int, val breathesAir: Boolean)
As with regular classes, if you want a property to be mutable you must preface it with the var
keyword.
case class Frog9c(canSwim: Boolean, var numLegs: Int, breathesAir: Boolean)
The case class’s automatically generated companion object defines the default method called apply
so you don’t have to use the new
keyword in order to create an instance.
Notice that case classes also define a toString
method which prints out the values of all the properties
passed to the default constructor.
scala> val frog9a = Frog9a(canSwim=true, 4, breathesAir=true) frog9a: Frog9a = Frog9a(true,4,true)
scala> val frog9b = Frog9b(canSwim=true, 4, breathesAir=true) frog9b: Frog9bb = Frog9b(true,4,true)
scala> val frog9c = Frog9c(canSwim=true, 4, breathesAir=true) frog10: Frog9c = Frog9c(true,4,true)
Object Equality

Let’s revisit the Dog
and Hog
comparison we saw
a few lectures ago, using case classes.
Case classes automatically define a method called canEqual
,
which informs us if two objects can be compared.
You must pass an instance of the class to be compared into canEqual
,
not a class name.
scala> case class Dog(name: String) defined class Dog
scala> case class Hog(name: String) defined class Hog
Case classes automatically define a method called canEqual
,
which informs us if two objects can be compared.
You must pass an instance of the class to be compared into canEqual
, not a class name.
scala> val dog1 = Dog("Fido") dog1: Dog = Dog(Fido)
scala> val dog2 = Dog("Fifi") dog2: Dog = Dog(Fifi)
scala> dog1.canEqual(dog2) res4: Boolean = true
scala> dog1==dog2 res1: Boolean = false
To clarify, an instance must be provided to compare, not a class name.
Unfortunately, false
is returned from this erroneous expression,
instead of throwing an exception.
scala> dog1.canEqual(Dog) res2: Boolean = false
Let’s say we want dogs with the same value of name
to be equivalent,
using a case-insensitive match.
definition of Dog
.
case class Dog(name: String) { override def equals(that: Any): Boolean = canEqual(that) && name.toLowerCase==that.name.toLowerCase override def hashCode = name.toLowerCase.hashCode }
Comparisons work as expected.
scala> Dog("fido")==Dog("Fido") res7: Boolean = true
Now let’s define Hog
.
case class Hog(name: String) { override def equals(that: Any): Boolean = canEqual(that) && hashCode==that.hashCode override def hashCode = name.hashCode }
Now let’s try to compare a dog with a hog:
scala> val hog1 = Hog("Porky") hog1: Hog = Hog(Porky)
scala> dog1.canEqual(hog1) // Again, provide an instance to compare, not a class: dog1.canEqual(Hog) is wrong res7: Boolean = false
scala> dog1==hog1 res9: Boolean = false
Arity 22 Limitation - Removed in Scala 2.11
The primary constructors for case classes can only accept a maximum of 22 properties. The implications of this are that database records with 23 or more fields cannot be persisted to a database. The arity 22 limitation of case classes has been removed in Scala 2.11.
Subclassing Traits and Classes into Case Classes
This is straightforward. Case classes can extend regular classes, abstract classes and traits, in exactly the same way that normal classes can extend other classes and traits. Here is an example of how to subclass a regular class as a case class.
abstract class AbstractFrog(canSwim: Boolean, numLegs: Int, breathesAir: Boolean) { override def toString = s"canSwim: $canSwim, numLegs=$numLegs, breathesAir=$breathesAir" }
case class Frog10(canSwim: Boolean, numLegs: Int, breathesAir: Boolean) extends AbstractFrog(canSwim, numLegs, breathesAir)
Now lets create an instance:
scala> val frog10 = Frog10(canSwim=true, 4, breathesAir=true)
frog10: Frog10 = canSwim: true, numLegs=4, breathesAir=true
Example from Play Framework
Play Framework defines a trait that looks something like the following.
It does not matter how EssentialFilter
is defined for the purposes of this explanation.
I’ve used a bogus definition for simplicity.
class EssentialFilter
trait HttpFilters { def filters: Seq[EssentialFilter] }
Now for the important bit.
A subclass of HttpFilters
is defined, called DefaultHttpFilters
,
which fulfills the HttpFilter
trait’s contract by implementing the filters
property as a val
.
We learned why this is allowable when we discussed the uniform access principle in the
Setters, Getters and the Uniform Access Principle lecture.
The collection of EssentialFilter
was expressed as a varargs parameter, discussed in the
Classes Part 2 lecture.
class DefaultHttpFilters(val filters: EssentialFilter*) extends HttpFilters
A case class could have been used instead used (without the val
prefix) like this:
case class DefaultHttpFilters(filters: EssentialFilter*) extends HttpFilters
Do Not Subclass Case Classes
Case classes must not be subclassed.
The compiler will prevent this to some extent, and you should not do so even when it lets you do so.
Instead, express your type hierarchy using traits and normal classes, and consider case classes as the equivalent of Java’s final
.
Here is the error you get if you attempt to subclass a case class:
scala> case class nope(override val canSwim: Boolean, override val numLegs: Int, override val breathesAir: Boolean) extends Frog10(canSwim, numLegs, breathesAir) <console>:10: error: case class nope has case ancestor Frog10, but case-to-case inheritance is prohibited. To overcome this limitation, use extractors to pattern match on non-leaf nodes.
Operator Overloading Revisited
Lets rewrite the complex arithmetic example we saw in the lecture about Scala classes, and use case classes instead.
Notice how the usage of the case class reads better because the new
keyword is not required.
case class Complex(re: Double, im: Double) { def + (another : Complex) = Complex(re + another.re, im + another.im)
def unary_- = Complex(-re, -im)
override def toString = s"${re} + ${im}i" }
Now let’s use this definition in some complex arithmetic operations:
scala> Complex(2, 5) + Complex(1, -2) res10: Complex = 3.0 + 3.0i
scala> -Complex(1, -2) res11: Complex = -1.0 + 2.0i
Standard Methods
The example code for each method assumes the following definitions, which were defined earlier in this lecture.
case class Frog9a(canSwim: Boolean, numLegs: Int, breathesAir: Boolean) case class Frog9c(canSwim: Boolean, var numLegs: Int, breathesAir: Boolean)
val frog9a = Frog9a(canSwim=false, numLegs=4, breathesAir=true) val frog9c = Frog9c(canSwim=true, numLegs=4, breathesAir=true)
Companion Classes

Case classes are guaranteed to have at least the following methods defined:
-
copy
- Copies a case class instance while allowing named properties to be modifiedScala REPLscala> val frog9a2 = frog9a.copy(canSwim=true) frog9a2: Frog9a = Frog(true,4,true)
scala> val frog9a3 = frog9a.copy(canSwim=true, numLegs=2) frog9a3: Frog9a = Frog9a(true,2,true) -
canEqual
- indicates if two objects can be compared.Scala REPLscala> Frog9a(true, 4, true).canEqual(Frog9c(true,4,true)) res1: Boolean = false
scala> frog9a2 canEqual frog9a3 res2: Boolean = true -
equals
- compare two objects. No error is given if they should not be compared - in that case,false
is quietly returned. This is an alias for==
.Scala REPLscala> frog9a.equals(frog9c) res3: Boolean = false
scala> frog9a equals frog9c res4: Boolean = false
scala> frog9a == frog9c res5: Boolean = false -
hashCode -
A digest stores a hash of the data from an instance of the class into a single hash value. Hashcodes are the basis of equality tests - if two objects have the samehashCode
then they areequal
.Scala REPLscala> frog9a.hashCode res5: Int = 272580090
-
productArity
- a count of the number of constructor properties of the case class.Scala REPLscala> frog9a.productArity res6: Int = 3
-
productElement
- retrieve the untyped value of the nth case class constructor property.Scala REPLscala> frog9a.productElement(0) res7: Any = true
scala> frog9a.productElement(1) res8: Any = 4
scala> frog9a.productElement(2) res9: Any = true -
productIterator
- return an iterator of the case class constructor properties.Scala REPLscala> frog9a.productIterator.foreach(println) // prints property values false 4 true
-
productPrefix
- Simply the name of the case class.Scala REPLscala> frog9a.productPrefix res10: String = Frog9a
-
toString
- string representation of the case class instance. Often overridden.Scala REPLscala> frog9a.toString res11: String = Frog9a(true,4,true)
scala> frog9a res12: Frog9a = Frog9a(true,4,true)
Companion Objects

Case class companion objects are guaranteed to have at least the following methods defined.
-
apply
– the default method for the companion object, which is a factory that callsnew
to construct new instances. -
toString
– prints the name of the case class. You normally should use the instance’stoString
method instead. -
unapply
– useful for pattern matching and value extraction. Unapply is devoted to this topic.
If you define a companion object for a case class, the methods and properties you define in the companion object will augment the default methods and properties in the automatically generated companion object.
Case Object
A case object
is a singleton case class
without a parameter list.
scala> case object MyFrog extends AbstractFrog(true, 2, true) defined object MyFrog
Caution - Defining Types within Methods or Functions
It is not a good idea to define traits, classes or case classes inside methods or functions. The following shows an attempt to define a case class inside a method and then return an instance. The Scala compiler’s error message is long, convoluted, and completely misleading.
scala> def foo = { case class Bar(a: String); Bar } <console>:7: warning: inferred existential type Bar.type forSome { val Bar: scala.runtime.AbstractFunction1[String,Bar] with Serializable{case def unapply(x$0: Bar): Option[String]}; type Bar <: Product with Serializable{val a: String; def copy(a: String): Bar; def copy$default$1: String @scala.annotation.unchecked.uncheckedVariance} }, which cannot be expressed by wildcards, should be enabled by making the implicit value scala.language.existentials visible. This can be achieved by adding the import clause ’import scala.language.existentials’ or by setting the compiler option -language:existentials. See the Scala docs for value scala.language.existentials for a discussion why the feature should be explicitly enabled. def foo = { case class Bar(a: String); Bar } ^ <console>:5: error: type mismatch; found : Bar.type(in lazy value $result) where type Bar.type(in lazy value $result) <: scala.runtime.AbstractFunction1[String,Bar] with Serializable{case def unapply(x$0: Bar): Option[String]} with Singleton required: (some other)Bar.type(in lazy value $result) forSome { type (some other)Bar.type(in lazy value $result) <: scala.runtime.AbstractFunction1[String,Bar] with Serializable{case def unapply(x$0: Bar): Option[String]} with Singleton; type Bar <: Product with Serializable{val a: String; def copy(a: String): Bar; def copy$default$1: String} } lazy val $result = foo
^ <console>:5: error: type mismatch; found : Bar.type(in value $result) where type Bar.type(in value $result) <: scala.runtime.AbstractFunction1[String,Bar] with Serializable{case def unapply(x$0: Bar): Option[String]} with Singleton required: Bar.type(in lazy value $result) forSome { type Bar.type(in lazy value $result) <: scala.runtime.AbstractFunction1[String,Bar] with Serializable{case def unapply(x$0: Bar): Option[String]} with Singleton; type Bar <: Product with Serializable{val a: String; def copy(a: String): Bar; def copy$default$1: String} } lazy val $result = foo
© Copyright 1994-2024 Michael Slinn. All rights reserved.
If you would like to request to use this copyright-protected work in any manner,
please send an email.
This website was made using Jekyll and Mike Slinn’s Jekyll Plugins.