Is there any difference between case object and object in scala?
A huge necro, but it is the highest result for this question in Google outside official tutorial which, as always, is pretty vague about the details. Here are some bare bones objects:
object StandardObject
object SerializableObject extends Serializable
case object CaseObject
Now, lets use the very useful feature of IntelliJ 'decompile Scala to Java' on compiled .class files:
//decompiled from StandardObject$.class
public final class StandardObject$ {
public static final StandardObject$ MODULE$ = new StandardObject$();
private StandardObject$() {
}
}
//decompiled from StandardObject.class
import scala.reflect.ScalaSignature;
@ScalaSignature()
public final class StandardObject {
}
As you can see, a pretty straightforward singleton pattern, except for reasons outside the scope of this question, two classes are generated: the static StandardObject
(which would contain static forwarder methods should the object define any) and the actual singleton instance StandardObject$
, where all methods defined in the code end up as instance methods. Things get more intresting when you implement Serializable
:
//decompiled from SerializableObject.class
import scala.reflect.ScalaSignature;
@ScalaSignature()
public final class SerializableObject {
}
//decompiled from SerializableObject$.class
import java.io.Serializable;
import scala.runtime.ModuleSerializationProxy;
public final class SerializableObject$ implements Serializable {
public static final SerializableObject$ MODULE$ = new SerializableObject$();
private Object writeReplace() {
return new ModuleSerializationProxy(SerializableObject$.class);
}
private SerializableObject$() {
}
}
The compiler doesn't limit itself to simply making the 'instance' (non-static) class Serializable
, it adds a writeReplace
method. writeReplace
is an alternative to writeObject
/readObject
; what it does, it serializes a different object whenether the Serializable
class having this method is being serialized. On deserializention then, that proxy object's readResolve
method is invoked once it is deserialized. Here, a ModuleSerializableProxy
instance is serialized with a field carrying the Class[SerializableObject]
, so it knows what object needs to be resolved. The readResolve
method of that class simply returns SerializableObject
- as it is a singleton with a parameterless constructor, scala object
is always structurally equal to itself between diffrent VM instances and different runs and, in this way, the property that only a single instance of that class is created per one VM instance is preserved. A thing of note is that there is a security hole here: no readObject
method is added to SerializableObject$
, meaning an attacker can maliciously prepare a binary file which matches standard Java serialization format for SerializableObject$
and a separate instance of the 'singleton' will be created.
Now, lets move to the case object
:
//decompiled from CaseObject.class
import scala.collection.Iterator;
import scala.reflect.ScalaSignature;
@ScalaSignature()
public final class CaseObject {
public static String toString() {
return CaseObject$.MODULE$.toString();
}
public static int hashCode() {
return CaseObject$.MODULE$.hashCode();
}
public static boolean canEqual(final Object x$1) {
return CaseObject$.MODULE$.canEqual(var0);
}
public static Iterator productIterator() {
return CaseObject$.MODULE$.productIterator();
}
public static Object productElement(final int x$1) {
return CaseObject$.MODULE$.productElement(var0);
}
public static int productArity() {
return CaseObject$.MODULE$.productArity();
}
public static String productPrefix() {
return CaseObject$.MODULE$.productPrefix();
}
public static Iterator productElementNames() {
return CaseObject$.MODULE$.productElementNames();
}
public static String productElementName(final int n) {
return CaseObject$.MODULE$.productElementName(var0);
}
}
//decompiled from CaseObject$.class
import java.io.Serializable;
import scala.Product;
import scala.collection.Iterator;
import scala.runtime.ModuleSerializationProxy;
import scala.runtime.Statics;
import scala.runtime.ScalaRunTime.;
public final class CaseObject$ implements Product, Serializable {
public static final CaseObject$ MODULE$ = new CaseObject$();
static {
Product.$init$(MODULE$);
}
public String productElementName(final int n) {
return Product.productElementName$(this, n);
}
public Iterator productElementNames() {
return Product.productElementNames$(this);
}
public String productPrefix() {
return "CaseObject";
}
public int productArity() {
return 0;
}
public Object productElement(final int x$1) {
Object var2 = Statics.ioobe(x$1);
return var2;
}
public Iterator productIterator() {
return .MODULE$.typedProductIterator(this);
}
public boolean canEqual(final Object x$1) {
return x$1 instanceof CaseObject$;
}
public int hashCode() {
return 847823535;
}
public String toString() {
return "CaseObject";
}
private Object writeReplace() {
return new ModuleSerializationProxy(CaseObject$.class);
}
private CaseObject$() {
}
}
A lot more is going on, as CaseObject$
now implements also Product0
, with its iterator and accessor methods. I am unaware of a use case for this feature, it is probably done for consistency with case class
which is always a product of its fields. The main practical difference here is that we get canEqual
, hashCode
and toString
methods for free. canEqual
is relevant only if you decide to compare it with a Product0
instance which is not a singleton object, toString
saves us from implementing a single simple method, which is useful when case objects are used as enumeration constants without any behaviour implemented. Finally, as one might suspect, hashCode
returns a constant, so it is the same for all VM instances. This matters if one serializes some flawed hash map implementation, but both standard java and scala hash maps wisely rehash all contents on deserialization, so it shouldn't matter. Note that equals
is not overriden, so it is still reference equality, and that the security hole is still there. A huge caveat here: if a case object inherit equals
/toString
from some supertype other than Object
, the corresponding methods are not generated, and the inherited definitions are used instead.
TL;DR: the only difference that matters in practice is the toString
returning the unqualified name of the object.
I must make a disclamer here, though: I cannot guarantee that the compiler doesn't treat case objects specially in addition to what is actually in the bytecode. It certainly does so when patterm matching case classes, aside from them implementing unapply
.