1. Overview

In this tutorial, we’ll explore the concepts of case object and object in Scala.

We’ll see the core differences between them and discuss the additional features of case object.

2. Scala Object

A Scala object represents a class that has exactly one single instance. An object is a singleton as a top-level value.

Here is an example of an object:

object Car {
  val numberOfWheels = 4

  def run(): Unit = {
    val currentDateAndTime: Date = new Date(System.currentTimeMillis())
    println(s"I am a new car running on $currentDateAndTime!")
  }
}

The big difference between object and class is that in object, we cannot assign parameters on constructors. Creating a Scala object as a singleton will be most useful for immutable instances and will only exist once.

3. Case Object Versus Object

In this chapter, we’ll first see a simple case object example. Then, we’ll compare the main differences between a case object and an object.

Let’s take a look at a simple case object:

case object Bicycle {
  val numberOfWheels = 2

  def run(): Unit = {
    val currentDateAndTime: Date = new Date(System.currentTimeMillis())
  }
}

As we can observe, *the only syntax difference from the previous example is the word case before object. A case object inherits all the features of objects, and extends them further:*

  • A default implementation of serialization
  • Pattern matching
  • Enumeration
  • A default implementation of toString

Let’s take a closer look at each of these.

3.1. Serialization

A case object is serializable by default, whereas an object is not.

class ObjectExampleUnitTest extends AnyFlatSpec {

  "Bicyle" should "be an instance of Serializable" in {
    assert(Bicycle.isInstanceOf[Serializable])
  }

  "Car" should "not be an instance of Serializable" in {
    assert(!Car.isInstanceOf[Serializable])
  }
}

To make an object serializable, we have to extend the Serializable trait:

object Car extends Serializable {
  val numberOfWheels = 4

  def run(): Unit = {
    val currentDateAndTime: Date = new Date(System.currentTimeMillis())
  }
}

3.2. Pattern Matching

Next, we’ll take a look at another powerful feature of a case object — pattern matching.

First, we create a new abstract class Vehicle and extend Bicycle and Car to it:

abstract class Vehicle
object Car extends Vehicle
case object Bicycle extends Vehicle

Second, we create a new method called messageVehicle:

def messageVehicle(vehicle: Vehicle): Unit = {
  vehicle match {
    case Car => println("send message to Car")
    case Bicycle => println("send message to Bicycle")
  }
}

This method gets a single parameter, which could be a Car or Bicycle. The pattern matching will decide the println output based on the type from a concrete instance of Vehicle.

For example, if we execute:

messageVehicle(Car)

We’ll get the output:

send message to Car

3.3. Enumeration

We can create an Enumeration using both an object and a case object. However, there are big differences in the way we implement them.

First, let’s take a look at how we implement Enumeration using object:

object FlyingObject extends Enumeration {
  val airplane = Value("AP")
  val bird = Value("BD")
  val drone = Value("DE")
}

As we can see from the previous example, in Scala, there is no enum keyword, unlike in Java. It simply offers us the Enumeration class. By extending it, we can iterate the members of the class. Moreover, we can access each element by calling its value. Or we can even access it by calling its ID:

${FlyingObject.bird.id}

By default, IDs are created incrementally for the enumerations. We can change their IDs:

object FlyingObjectChangingID extends Enumeration {
  val airplane = Value(2, "AP")
  val bird = Value(3, "BD")
  val drone = Value(1, "DE")
}

Now, the order of the enumerations is “DE, AP, BD”. As we can see, the order of the values is now different from the first printing iteration.

Let’s see a problem with enumerations while doing pattern matching.

There is no exhaustive matching check on compile time. Thus, if we write:

def nonExhaustive(objects: FlyingObject.Value) {
  objects match {
    case FlyingObject.airplane => println("I am an airplane")
    case FlyingObject.bird => println("I am a bird")
  }
}

nonExhaustive(FlyingObject.drone)

The code will compile without any error, but it will throw a scala.MatchError at runtime.

To avoid this issue, we can use a sealed case object. Let’s take a look at an example:

sealed trait FlyingCaseObjects
case object AirplaneCaseObject extends FlyingCaseObjects
case object BirdCaseObject extends FlyingCaseObjects
case object DroneCaseObject extends FlyingCaseObjects

We can only extend sealed traits in the same file. The compiler will know all possible subtypes and provide warnings in case pattern matching is not exhaustive. The following method will emit an exhaustive warning:

def sealedTraitMatch(flyingObject: FlyingCaseObjects): Unit = {
  flyingObject match {
    case AirplaneCaseObject => println("I am an airplane")
    case BirdCaseObject => println("I am a bird")
  }
}

3.4. Implementation of the toString Method

Finally, let’s look at the last additional feature of a case object over an object — the default implementation of the toString method:

println(Car)
println(Bicycle)

This will trigger the toString method, which provides us the name of the object by default. Now, we see the output:

com.baeldung.scala.caseobject.Car$@2f7c7260
Bicycle

4. Conclusion

In this tutorial, we’ve seen how we can use case object and object for different purposes.

First, we saw the syntax differences between object and case object. Then we looked at different ways to create pattern matching and enumerations.

Finally, we explored the default implementation of the toString method for case object.

As always, the code can be found over on GitHub.