Difference between case object and object

后端 未结 7 504
梦如初夏
梦如初夏 2020-12-02 04:36

Is there any difference between case object and object in scala?

7条回答
  •  失恋的感觉
    2020-12-02 04:58

    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.

提交回复
热议问题