在 Java 中,静态成员并不是通过实例去调用的,而是通过类名调用的,关键字是 static 。严格意义上来说,静态成员并不满足 OOP 的思想:它本质上和这个类并没有任何关联。或者说,它的存在更像是一个 PO 的全局变量。
由于当时所处的时代限制, Java 语言不得不兼顾一些 PO 思想的内容。而对于 Scala 这门多范式语言,它除了实现 FP 之外,还将 OOP 发挥到了极致。Scala 之父马丁·奥德斯基在设计该语言时,便将 static 这个被 OOP 视为“眼中钉” 的概念移除了。
然而,有时候我们确实需要脱离于某个具体对象的,一个静态的全局变量。为了弥补消除 static 所带来的缺陷(或者说让它看起来更 OOP 一点),马丁·奥德斯基引入了 伴生对象 的概念:在 Scala 中,类的“动静分明”。一切非静态内容,保存在 class 中,而静态内容则保存在 object 中。
这也是将 Scala 的主函数声明在一个 object 内的原因:因为主函数是 “静态” 的,单例的。另外,在有些资料中,也称伴生对象是单例对象。
如何声明一个伴生对象
Scala 的世界里没有 static 关键字,也没有和静态有关的概念。不过鉴于我们学习 Java 的经验,笔者在下文仍然会使用 “静态” 一词来阐述一些概念。
首先给出一个伴生对象的例子,我们再去叙述其细节。
object Associated_Object { def main(args: Array[String]): Unit = {}}class Associated_Object{}
当同一个文件内同时存在 object x 和 class x 的声明是:
- 我们称 class x 称作 object x 的 伴生类 。
- 其 object x 称作 class x 的 伴生对象 。
伴生类和伴生对象是相对的概念。
在编译时,伴生类 object x 被编译成了 x.class ,而伴生对象 object x 被编译成了 x$.class 。
对于声明在伴生对象的成员,可以直接通过类名来调用。比如在伴生对象内声明了一个属性:
object Associated_Object { val publicKey: String = "Associated value"
那么在主函数中,可以直接用类名去调用 publicKey 的值:
println(s"args = ${Associated_Object.publicKey}")
同样的,伴生对象也可以声明 private 的成员。 这样,这个被修饰的成员仅可以被伴生类的实例所访问,并且所有的伴生类实例共享这一个变量 。
伴生类的apply方法
查看下面的代码。 new 关键字哪去了?
val cat : Cat = Cat()
这种写法是隐式地调用了其伴生对象的 apply 方法。下面给出 Cat 类的完整声明:
class Cat(){}object Cat{ //该方法内置了构造方法。 def apply() : Cat = new Cat()}
你也可以按照 简单工厂模式 的思路去理解它:调用该 Cat 伴生对象的 apply 方法 ,并返回一个实例。
注意:
- 只有在伴生对象内声明 apply 方法之后,后续可以不带 new 关键字直接创建实例,这相当于是 Scala 的语法糖。
- apply 方法允许携带参数, 返回值绝大部分情况下都应该是本类的一个实例 ,但是 Scala 对此并没有在编译角度上做严格规定。
笔者极力建议遵守第二条规则,以避免下面的这种混乱情况:
object Cat{ //这样的代码从编译角度来看没有任何问题。 def apply() : Dog = new Dog()}//------Main-----------但是,会让代码的调用者陷入混乱。//在99.9999% 的情况下,apply 方法返回的都应该是其对应类的实例。val dog : Dog = Cat
另外,Scala 还有一个 unapply 方法,或称之为提取器。我们在后续的模式匹配章节会正式提到它。
可以只声明伴生对象,而不声明伴生类吗?
当然!实际上我们之前写案例时,大部分时间都是只声明一个 object ,然后直接里面声明主函数逻辑。
从编译的角度看,会出现这样一个有趣的现象:
凡是用 object 修饰的伴生对象 x ,编译后一定会生成两个文件:一个是 x.class ,另一个文件是 x$.class 文件。即便没有使用 class 声明伴生类,编译器在底层仍然会生成内容为空的 x.class 文件。
只使用一个 class 修饰的类,在编译时只会生成一个 x.class 文件。后文给出了如何用 Java 代码实现 Scala 的伴生对象和伴生类,这个实例有助于你理解为什么 Scala 会编译出两个文件。
我们是否可以只依赖单独的 object ?
伴生对象本身实现了单例模式。因此有人又称伴生对象是单例对象,也是有规可循的。比如:
object Single { def fun():Unit = println("this is a singleton.")}
这样,我们在程序中的 Single 符号都指代这个单例对象,并且可以使用 . 运算符直接调用内部公开的属性和方法。
经过笔者的验证,可以在单例对象上直接声明继承关系。为了通过编译,我们要把下面的 Parent 和 Single 写在一个 .scala 文件内部。
class Parent { def greet() : Unit = println("hello")}object Single extends Parent { def fun():Unit = println("this is a singleton.")}
这样,我们可以直接通过 Single.greet() 方法来调用它从 Parent 继承来的方法。 但显然,在 object 单例对象中声明继承关系显得不伦不类 。为什么?因为这本来不是伴生对象原本的用途。
然而,编译器却 “放行” 了这样不规范的代码,因为从编译的角度来看确实没有任何问题。但是它会令初学者(笔者)混乱,比如:
- 继承关系和构造器写在 object 和写在 class 有没有区别?
- 如果有,那它们之间会存在哪些区别?
- 如果同时在一对 object 和 class 声明不同的继承关系,这会不会是一个多重继承?
笔者在这里通过实际上手代码的方式来一一验证。
单例对象没有带参构造器
对刚才的 Single 稍作修改后,笔者发现:不能在单例对象上声明任何构造器。下面的写法并不能通过:
object Single(val int : Int) { def fun():Unit = println("this is a singleton.")}
编辑器会反馈上述的代码存在语法错误。这说明,Scala 的设计者马丁·奥德斯基对伴生对象做了一些编译层面的限制。Single 类如果需要自定义的构造器,它必须要依赖其 class 修饰的伴生类来实现:
object Single {def fun():Unit = println("this is a singleton.")}class Single(val int : Int)
警惕 Scala 的障眼法
能不能在 class 和 object 分别声明继承关系,以此实现一个多重继承呢?笔者给出了下方的"问题代码":(为了通过编译,这些类声明要写在一个 .scala 文件中)
class Fatherobject Single extends Father { def fun():Unit = println("this is a singleton.")}class Motherclass Single(val int : Int) extends Mother
编译是成功的!这一看似乎是 Single 同时继承了 Father 和 Mother 。真相是如此吗?
尽管伴生对象和伴生类这一对概念用于修饰一个类的 “静态” 和 "动态" 部分, 但是 Scala 在编译时,是将伴生对象和伴生类分开编译的 (即前文提到的 x.class 和 x$.class )。
因此两者实际上只是共享了一个类名,而伴生对象替伴生类保存着一些 ”静态“ 变量和方法,充当着 “仓库” 的作用。
为了避免不必要的混乱,我们在同时使用伴生对象和伴生类去描述一个 “具备静态内容的类” 时,仅仅会将 ”静态“ 的成员放到 object 上,而继承关系,和构造器等内容的声明全部放到 class 上。
使用 Java 程序模拟伴生对象实现
为了直观理解编译器在底层是如何编译伴生对象和伴生类的,我们直接使用 Java 代码来模拟一次。在这里假设一个伴生类 Associated_Object ,它有一个 Integer 类型的静态属性: publicKey 。该成员声明在了它的伴生对象 Associated_Object$ 中。
- Associated_Object$ 在静态域 中构造了一个实例 MODULE$ ,这个实例使用 final 关键字来 保护它不会被更改内存地址 。
- Associated_Object 类同样有一个 静态方法 getPublicKey :它永远都指向 MODULE$ 内的 publicKey 。
经过上述的两个步骤,这相当于是构造了一个 单例模式 :即任意一个 Associated_Object 对象需要访问 publicKey 时,都是从静态域里的 MODULE$ 那里获取。
下面给出代码实现。
public final class Associated_Object$ { private Integer publicKey; //在静态域中直接指定MODULE$保存Associated_Object的静态属性。 static { MODULE$ = new Associated_Object$(); } public static final Associated_Object$ MODULE$; public Integer getPublicKey() { return MODULE$.publicKey; } public void setPublicKey(Integer publicKey) { MODULE$.publicKey = publicKey; } //在初始化中,对publicKey进行赋值。 //Associated_Object的静态属性会随着初始化保存到MODULE$当中。 Associated_Object$() { this.publicKey = 100; }}//--------------------------------------------------------------------//public class Associated_Object { private Integer InstanceKey; //非静态的属性保存在Associated_Object类本身,正常调用即可。 public Integer getInstanceKey() { return InstanceKey; } public void setInstanceKey(Integer instanceKey) { InstanceKey = instanceKey; } //Associate_Object类本身没有静态的"publicKey"属性。 //因此需要委托Associated_Object$的实例从MODULE$那获取相应的属性。 public static Integer getPublicKey() { return Associated_Object$.MODULE$.getPublicKey(); } //原理同 getPublicKey 方法。 public static void setPublicKey(Integer salary) { Associated_Object$.MODULE$.setPublicKey(salary); }}
小结
在本章节,我们了解了 Scala 伴生对象和伴生类的关系:
.class.scala
通过 Java 代码的实现可知,Scala 世界中的 ”静态“ 不过是障眼法罢了。当我们去调用所谓的 "静态" 内容时,实际上程序调用的是 object 单例对象(或称伴生对象,但是这里叫单例对象更加合适),和 class 伴生类没有什么联系。
只不过由于伴生对象和伴生类保持同一个名字,使得我们通过大写的 ”类名“ 进行调用的时候,根据学习 Java 的思维习惯,理所当然地将它理解成了 Single 类的”静态“成员。
然而,Scala 并没有规定伴生对象和伴生类一定要成对出现,我们可以仅定义 class ,也可以只定义 object 。当你仅需要实现一个简单的单例模式时,仅使用一个 object 最好不过了:比如说一个主函数的入口。
小试牛刀
首先,定义一个 Counter 类,它有一个静态属性 count 。当主函数启动时,每实例化一次 Counter 实例,就对 count 进行一次自增操作。
下列代码是 Java 实现。
public class Counters{ private static int count = 0; public Counters(){ count++; }}
而这个需求在 Scala 中的实现方式是这样的:
class Counter { Counter.count += 1}object Counter { var count : Int = 0}
不要忘记一件事情: Scala 的伴生对象和伴生类,从编译的角度看是独立的 。因此我们不能直接在伴生类中访问 count ,而是带上伴生对象的名字(尽管它们都叫一个名字): Counter.count 。
作者:var_Coder_