一个巨大的死灵,但它是谷歌官方教程之外的这个问题的最高结果,与往常一样,它对细节非常模糊。以下是一些简单的对象:
object StandardObject
object SerializableObject extends Serializable
case object CaseObject
现在,让我们在已编译的 .class 文件上使用 IntelliJ '将 Scala 反编译为 Java' 的非常有用的功能:
//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(<byte array string elided>)
public final class StandardObject {
}
如您所见,一个非常简单的单例模式,除了这个问题范围之外的原因,两个类被生成:静态StandardObject(如果object define any) 和实际的单例实例StandardObject$,其中代码中定义的所有方法最终都作为实例方法。实现 Serializable 后事情会变得更有趣:
//decompiled from SerializableObject.class
import scala.reflect.ScalaSignature;
@ScalaSignature(<byte array string elided>)
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$() {
}
}
编译器并不局限于简单地创建“实例”(非静态)类Serializable,它添加了一个writeReplace 方法。 writeReplace 是writeObject/readObject 的替代品;它的作用是在序列化具有此方法的Serializable 类时序列化不同的对象。然后在反序列化时,该代理对象的 readResolve 方法在反序列化后被调用。这里,ModuleSerializableProxy 实例被序列化为带有Class[SerializableObject] 的字段,因此它知道需要解析哪个对象。该类的 readResolve 方法只返回 SerializableObject - 因为它是一个带有无参数构造函数的单例,scala object 在不同的 VM 实例和不同的运行之间总是在结构上等于自身,并且通过这种方式,属性每保留一个 VM 实例,只会创建该类的一个实例。需要注意的是这里有一个安全漏洞:没有readObject 方法被添加到SerializableObject$,这意味着攻击者可以恶意准备一个二进制文件,它与SerializableObject$ 的标准Java 序列化格式和一个单独的实例相匹配。将创建“单例”。
现在,让我们转到case object:
//decompiled from CaseObject.class
import scala.collection.Iterator;
import scala.reflect.ScalaSignature;
@ScalaSignature(<byte array string elided>)
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$() {
}
}
还有很多事情要做,因为CaseObject$ 现在也实现了Product0,带有它的迭代器和访问器方法。我不知道此功能的用例,它可能是为了与case class 保持一致,这始终是其领域的产物。这里主要的实际区别是我们免费获得canEqual、hashCode 和toString 方法。 canEqual 仅在您决定将其与不是单例对象的 Product0 实例进行比较时才相关,toString 使我们免于实现单个简单方法,这在将 case 对象用作没有任何枚举常量的情况下很有用行为实施。最后,正如人们可能怀疑的那样,hashCode 返回一个常量,因此对于所有 VM 实例都是相同的。如果一个人序列化一些有缺陷的哈希映射实现,这很重要,但是标准的 java 和 scala 哈希映射都明智地在反序列化时重新哈希所有内容,所以这无关紧要。请注意,equals 没有被覆盖,所以它仍然是引用相等,并且安全漏洞仍然存在。这里有个很大的警告:如果一个 case 对象从 Object 以外的某个超类型继承 equals/toString,则不会生成相应的方法,而是使用继承的定义。
TL;DR:在实践中唯一重要的区别是 toString 返回对象的非限定名称。
不过,我必须在此声明:除了字节码中的实际内容之外,我不能保证编译器不会专门处理 case 对象。除了实现unapply之外,在模式匹配案例类时肯定会这样做。