4.3 单例对象
正如第1章提到的,Scala比Java更面向对象的一点是,Scala的类不允许有静态(static)成员。对于此类使用场景,Scala提供了单例对象。单例对象的定义看上去与类定义很像,只不过class关键字被替换成了object关键字。参考示例4.2。
示例4.2 ChecksumAccumulator类的伴生对象
在示例4.2中的单例对象名为ChecksumAccumulator,与前一个示例中的类名一样。当单例对象与某个类共用同一个名称时,它被称作这个类的伴生对象(companion object)。必须在同一个源码文件中定义类和类的伴生对象。同时,类又叫作这个单例对象的伴生类(companion class)。类和它的伴生对象可以互相访问对方的私有成员。
ChecksumAccumulator单例对象有一个名称为calculate的方法,用于接收一个String,并计算这个String的所有字符的校验和(checksum)。它同样也有一个私有的字段,即cache,这是一个缓存了之前已计算过的校验和的可变映射。[3]方法的第一行,即“if (cache.contains(s))”,用于检查缓存以确认传入的字符串是否已经被包含在映射中。如果是,就返回映射的值,即cache(s)。如果没有,则执行else子句,计算校验和。else子句的第一行定义了一个名称为acc的val,用一个新的ChecksumAccumulator实例初始化。[4]接下来的一行是一个for表达式,遍历传入字符串的每一个字符,通过调用toByte方法将字符转换成Byte,然后将Byte传递给acc指向的ChecksumAccumulator实例的add方法。[5]在for表达式执行完成以后,方法的下一行调用acc的checksum方法,从传入的String中得到其校验和,保存到名称为cs的val。再往下一行,即cache += (s -> cs),将传入的字符串作为键,计算出的整型的校验和作为值,这组键/值对被添加到缓存映射中。该方法的最后一个表达式,即cs,确保了该方法的结果是这个校验和。
如果你是Java程序员,则可以把单例对象当作用于安置那些用Java时打算编写的静态方法。可以用类似的方式访问单例对象的方法:单例对象名、英文句点和方法名。例如,可以像这样调用ChecksumAccumulator单例对象的calculate方法:
不过,单例对象并不仅仅用来存放静态方法。它是一等(first-class)的对象。可以把单例对象的名称想象成附加在对象身上的“名称标签”:
定义单例对象并不会定义类型(在Scala的抽象层级上是这样的)。当只有ChecksumAccumulator的对象定义时,并不能定义一个类型为ChecksumAccumulator的变量。确切地说,名称为ChecksumAccumulator的类型是由这个单例对象的伴生类来定义的。不过,单例对象可以扩展自某个超类,还可以混入特质。你可以通过这些类型来调用它的方法,用这些类型的变量来引用它,还可以将它传入那些预期为这些类型的入参的方法中。我们将在第12章给出单例对象继承类和特质的示例。
类和单例对象的一个区别是单例对象不接收参数,而类可以。由于无法用new实例化单例对象,也就没有任何手段来向它传参。每个单例对象都是通过一个静态变量引用合成类(synthetic class)的实例来实现的,因此单例对象在初始化的语义上与Java的静态成员是一致的。[6]尤其体现在,单例对象在有代码首次访问时才会被初始化。
不与某个伴生类共用同一个名称的单例对象叫作独立对象(standalone object)。独立对象有很多用途,包括收集相关的工具方法,或者定义Scala应用程序的入口,等等。下一节将介绍这样的用法。