Scala编程(第5版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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子句的第一行定义了一个名称为accval,用一个新的ChecksumAccumulator实例初始化。[4]接下来的一行是一个for表达式,遍历传入字符串的每一个字符,通过调用toByte方法将字符转换成Byte,然后将Byte传递给acc指向的ChecksumAccumulator实例的add方法。[5]for表达式执行完成以后,方法的下一行调用accchecksum方法,从传入的String中得到其校验和,保存到名称为csval。再往下一行,即cache += (s -> cs),将传入的字符串作为键,计算出的整型的校验和作为值,这组键/值对被添加到缓存映射中。该方法的最后一个表达式,即cs,确保了该方法的结果是这个校验和。

如果你是Java程序员,则可以把单例对象当作用于安置那些用Java时打算编写的静态方法。可以用类似的方式访问单例对象的方法:单例对象名、英文句点和方法名。例如,可以像这样调用ChecksumAccumulator单例对象的calculate方法:

不过,单例对象并不仅仅用来存放静态方法。它是一等(first-class)的对象。可以把单例对象的名称想象成附加在对象身上的“名称标签”:

定义单例对象并不会定义类型(在Scala的抽象层级上是这样的)。当只有ChecksumAccumulator的对象定义时,并不能定义一个类型为ChecksumAccumulator的变量。确切地说,名称为ChecksumAccumulator的类型是由这个单例对象的伴生类来定义的。不过,单例对象可以扩展自某个超类,还可以混入特质。你可以通过这些类型来调用它的方法,用这些类型的变量来引用它,还可以将它传入那些预期为这些类型的入参的方法中。我们将在第12章给出单例对象继承类和特质的示例。

类和单例对象的一个区别是单例对象不接收参数,而类可以。由于无法用new实例化单例对象,也就没有任何手段来向它传参。每个单例对象都是通过一个静态变量引用合成类synthetic class)的实例来实现的,因此单例对象在初始化的语义上与Java的静态成员是一致的。[6]尤其体现在,单例对象在有代码首次访问时才会被初始化。

不与某个伴生类共用同一个名称的单例对象叫作独立对象standalone object)。独立对象有很多用途,包括收集相关的工具方法,或者定义Scala应用程序的入口,等等。下一节将介绍这样的用法。