What is the difference between Scala's case class and class?

Viewed 142154

I searched in Google to find the differences between a case class and a class. Everyone mentions that when you want to do pattern matching on the class, use case class. Otherwise use classes and also mentioning some extra perks like equals and hash code overriding. But are these the only reasons why one should use a case class instead of class?

I guess there should be some very important reason for this feature in Scala. What is the explanation or is there a resource to learn more about the Scala case classes from?

17 Answers

Case classes can be seen as plain and immutable data-holding objects that should exclusively depend on their constructor arguments.

This functional concept allows us to

  • use a compact initialization syntax (Node(1, Leaf(2), None)))
  • decompose them using pattern matching
  • have equality comparisons implicitly defined

In combination with inheritance, case classes are used to mimic algebraic datatypes.

If an object performs stateful computations on the inside or exhibits other kinds of complex behaviour, it should be an ordinary class.

Technically, there is no difference between a class and a case class -- even if the compiler does optimize some stuff when using case classes. However, a case class is used to do away with boiler plate for a specific pattern, which is implementing algebraic data types.

A very simple example of such types are trees. A binary tree, for instance, can be implemented like this:

sealed abstract class Tree
case class Node(left: Tree, right: Tree) extends Tree
case class Leaf[A](value: A) extends Tree
case object EmptyLeaf extends Tree

That enable us to do the following:

// DSL-like assignment:
val treeA = Node(EmptyLeaf, Leaf(5))
val treeB = Node(Node(Leaf(2), Leaf(3)), Leaf(5))

// On Scala 2.8, modification through cloning:
val treeC = treeA.copy(left = treeB.left)

// Pretty printing:
println("Tree A: "+treeA)
println("Tree B: "+treeB)
println("Tree C: "+treeC)

// Comparison:
println("Tree A == Tree B: %s" format (treeA == treeB).toString)
println("Tree B == Tree C: %s" format (treeB == treeC).toString)

// Pattern matching:
treeA match {
  case Node(EmptyLeaf, right) => println("Can be reduced to "+right)
  case Node(left, EmptyLeaf) => println("Can be reduced to "+left)
  case _ => println(treeA+" cannot be reduced")
}

// Pattern matches can be safely done, because the compiler warns about
// non-exaustive matches:
def checkTree(t: Tree) = t match {
  case Node(EmptyLeaf, Node(left, right)) =>
  // case Node(EmptyLeaf, Leaf(el)) =>
  case Node(Node(left, right), EmptyLeaf) =>
  case Node(Leaf(el), EmptyLeaf) =>
  case Node(Node(l1, r1), Node(l2, r2)) =>
  case Node(Leaf(e1), Leaf(e2)) =>
  case Node(Node(left, right), Leaf(el)) =>
  case Node(Leaf(el), Node(left, right)) =>
  // case Node(EmptyLeaf, EmptyLeaf) =>
  case Leaf(el) =>
  case EmptyLeaf =>
}

Note that trees construct and deconstruct (through pattern match) with the same syntax, which is also exactly how they are printed (minus spaces).

And they can also be used with hash maps or sets, since they have a valid, stable hashCode.

  • Case classes can be pattern matched
  • Case classes automatically define hashcode and equals
  • Case classes automatically define getter methods for the constructor arguments.

(You already mentioned all but the last one).

Those are the only differences to regular classes.

No one mentioned that case classes are also instances of Product and thus inherit these methods:

def productElement(n: Int): Any
def productArity: Int
def productIterator: Iterator[Any]

where the productArity returns the number of class parameters, productElement(i) returns the ith parameter, and productIterator allows iterating through them.

Apart from what people have already said, there are some more basic differences between class and case class

1.Case Class doesn't need explicit new, while class need to be called with new

val classInst = new MyClass(...)  // For classes
val classInst = MyClass(..)       // For case class

2.By Default constructors parameters are private in class , while its public in case class

// For class
class MyClass(x:Int) { }
val classInst = new MyClass(10)

classInst.x   // FAILURE : can't access

// For caseClass
case class MyClass(x:Int) { }
val classInst = MyClass(10)

classInst.x   // SUCCESS

3.case class compare themselves by value

// For Class
class MyClass(x:Int) { }
 
val classInst = new MyClass(10)
val classInst2 = new MyClass(10)

classInst == classInst2 // FALSE

// For Case Class
case class MyClass(x:Int) { }
 
val classInst = MyClass(10)
val classInst2 = MyClass(10)

classInst == classInst2 // TRUE

To have the ultimate understanding of what is a case class:

let's assume the following case class definition:

case class Foo(foo:String, bar: Int)

and then do the following in the terminal:

$ scalac -print src/main/scala/Foo.scala

Scala 2.12.8 will output:

...
case class Foo extends Object with Product with Serializable {

  <caseaccessor> <paramaccessor> private[this] val foo: String = _;

  <stable> <caseaccessor> <accessor> <paramaccessor> def foo(): String = Foo.this.foo;

  <caseaccessor> <paramaccessor> private[this] val bar: Int = _;

  <stable> <caseaccessor> <accessor> <paramaccessor> def bar(): Int = Foo.this.bar;

  <synthetic> def copy(foo: String, bar: Int): Foo = new Foo(foo, bar);

  <synthetic> def copy$default$1(): String = Foo.this.foo();

  <synthetic> def copy$default$2(): Int = Foo.this.bar();

  override <synthetic> def productPrefix(): String = "Foo";

  <synthetic> def productArity(): Int = 2;

  <synthetic> def productElement(x$1: Int): Object = {
    case <synthetic> val x1: Int = x$1;
        (x1: Int) match {
            case 0 => Foo.this.foo()
            case 1 => scala.Int.box(Foo.this.bar())
            case _ => throw new IndexOutOfBoundsException(scala.Int.box(x$1).toString())
        }
  };

  override <synthetic> def productIterator(): Iterator = scala.runtime.ScalaRunTime.typedProductIterator(Foo.this);

  <synthetic> def canEqual(x$1: Object): Boolean = x$1.$isInstanceOf[Foo]();

  override <synthetic> def hashCode(): Int = {
     <synthetic> var acc: Int = -889275714;
     acc = scala.runtime.Statics.mix(acc, scala.runtime.Statics.anyHash(Foo.this.foo()));
     acc = scala.runtime.Statics.mix(acc, Foo.this.bar());
     scala.runtime.Statics.finalizeHash(acc, 2)
  };

  override <synthetic> def toString(): String = scala.runtime.ScalaRunTime._toString(Foo.this);

  override <synthetic> def equals(x$1: Object): Boolean = Foo.this.eq(x$1).||({
      case <synthetic> val x1: Object = x$1;
        case5(){
          if (x1.$isInstanceOf[Foo]())
            matchEnd4(true)
          else
            case6()
        };
        case6(){
          matchEnd4(false)
        };
        matchEnd4(x: Boolean){
          x
        }
    }.&&({
      <synthetic> val Foo$1: Foo = x$1.$asInstanceOf[Foo]();
      Foo.this.foo().==(Foo$1.foo()).&&(Foo.this.bar().==(Foo$1.bar())).&&(Foo$1.canEqual(Foo.this))
  }));

  def <init>(foo: String, bar: Int): Foo = {
    Foo.this.foo = foo;
    Foo.this.bar = bar;
    Foo.super.<init>();
    Foo.super./*Product*/$init$();
    ()
  }
};

<synthetic> object Foo extends scala.runtime.AbstractFunction2 with Serializable {

  final override <synthetic> def toString(): String = "Foo";

  case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);

  case <synthetic> def unapply(x$0: Foo): Option =
     if (x$0.==(null))
        scala.None
     else
        new Some(new Tuple2(x$0.foo(), scala.Int.box(x$0.bar())));

  <synthetic> private def readResolve(): Object = Foo;

  case <synthetic> <bridge> <artifact> def apply(v1: Object, v2: Object): Object = Foo.this.apply(v1.$asInstanceOf[String](), scala.Int.unbox(v2));

  def <init>(): Foo.type = {
    Foo.super.<init>();
    ()
  }
}
...

As we can see Scala compiler produces a regular class Foo and companion-object Foo.

Let's go through the compiled class and comment on what we have got:

  • the internal state of the Foo class, immutable:
val foo: String
val bar: Int
  • getters:
def foo(): String
def bar(): Int
  • copy methods:
def copy(foo: String, bar: Int): Foo
def copy$default$1(): String
def copy$default$2(): Int
  • implementing scala.Product trait:
override def productPrefix(): String
def productArity(): Int
def productElement(x$1: Int): Object
override def productIterator(): Iterator
  • implementing scala.Equals trait for make case class instances comparable for equality by ==:
def canEqual(x$1: Object): Boolean
override def equals(x$1: Object): Boolean
  • overriding java.lang.Object.hashCode for obeying the equals-hashcode contract:
override <synthetic> def hashCode(): Int
  • overriding java.lang.Object.toString :
override def toString(): String
  • constructor for instantiation by new keyword:
def <init>(foo: String, bar: Int): Foo 

Object Foo: - method apply for instantiation without new keyword:

case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);
  • extractor method unupply for using case class Foo in pattern matching:
case <synthetic> def unapply(x$0: Foo): Option
  • method to protect object as singleton from deserialization for not letting produce one more instance:
<synthetic> private def readResolve(): Object = Foo;
  • object Foo extends scala.runtime.AbstractFunction2 for doing such trick:
scala> case class Foo(foo:String, bar: Int)
defined class Foo

scala> Foo.tupled
res1: ((String, Int)) => Foo = scala.Function2$$Lambda$224/1935637221@9ab310b

tupled from object returns a funtion to create a new Foo by applying a tuple of 2 elements.

So case class is just syntactic sugar.

Unlike classes, case classes are just used to hold data.

Case classes are flexible for data-centric applications, which means you can define data fields in case class and define business logic in a companion object. In this way, you are separating the data from the business logic.

With the copy method, you can inherit any or all required properties from the source and can change them as you like.

I think overall all the answers have given a semantic explanation about classes and case classes. This could be very much relevant, but every newbie in scala should know what happens when you create a case class. I have written this answer, which explains case class in a nutshell.

Every programmer should know that if they are using any pre-built functions, then they are writing a comparatively less code, which is enabling them by giving the power to write most optimized code, but power comes with great responsibilities. So, use prebuilt functions with very cautions.

Some developers avoid writing case classes due to additional 20 methods, which you can see by disassembling class file.

Please refer this link if you want to check all the methods inside a case class.

Some of the key features of case classes are listed below

  1. case classes are immutable.
  2. You can instantiate case classes without new keyword.
  3. case classes can be compared by value

Sample scala code on scala fiddle, taken from the scala docs.

https://scalafiddle.io/sf/34XEQyE/0

One important issue not mentioned in earlier answers is that of identity. Objects of regular classes have identity, so even if two objects have identical values for all their fields, they are still different objects. For case class instances however, equality is defined in purely terms of the values of the object's fields.

Related