5.15 vs2019 静态编译_Scala之:伴生对象与静态概念

在 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 单例对象中声明继承关系显得不伦不类 。为什么?因为这本来不是伴生对象原本的用途。

然而,编译器却 “放行” 了这样不规范的代码,因为从编译的角度来看确实没有任何问题。但是它会令初学者(笔者)混乱,比如:

  1. 继承关系和构造器写在 object 和写在 class 有没有区别?
  2. 如果有,那它们之间会存在哪些区别?
  3. 如果同时在一对 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$ 中。

  1. Associated_Object$ 在静态域 中构造了一个实例 MODULE$ ,这个实例使用 final 关键字来 保护它不会被更改内存地址
  2. Associated_Object 类同样有一个 静态方法 getPublicKey :它永远都指向 MODULE$ 内的 publicKey 。

经过上述的两个步骤,这相当于是构造了一个 单例模式 :即任意一个 Associated_Object 对象需要访问 publicKey 时,都是从静态域里的 MODULE$ 那里获取。

b6a4c74390ad4e1716c06bfa8c114617.png

下面给出代码实现。

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_

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值