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