scala中的伴生对象实现原理

孤立对象是只有一个object关键字修饰的对象。 该对象会编译成两个class文件, 一个是以孤立对象的名字命名的class,  一个是以孤立对象的名字后面加上一个$字符命名的class, 这个class又叫做虚构类。 源码中的孤立对象中的字段和方法, 都被编译成以孤立对象的名字命名的class中的静态方法, 这些静态方法都会访问单例的虚构类对象。 虚构了是传统意义上的单例模式, 并且在类初始化的时候有, 就会创建唯一的对象。 源码中的所有字段和方法都会在虚构类中有相对应的成员。 如果不明白的可以回过头去看一下上一篇博客,
博客的链接已经在上面给出了。

本文主要分析伴生类和伴生对象的实现方法。 所谓伴生对象, 也是一个Scala中的单例对象, 使用object关键字修饰。 除此之外, 还有一个使用class关键字定义的同名类, 这个类和单例对象存在于同一个文件中, 这个类就叫做这个单例对象的伴生类, 相对来说, 这个单例对象叫做伴生类的伴生对象。

为了延续上一篇文章, 在本文中的示例还会使用上一篇博客中的单例对象, 只不过为这个单例对象定义一个伴生类。 代码如下所示:

[java] view
plain
 copy

  1. class Test{
  2. var field = "field"
  3. def doSomeThing = println("do something")
  4. }
  5. object  Test {
  6. val a = "a string";
  7. def printString = println(a)
  8. }

与上一篇博客相比, 这个示例程序多了一个伴生类。 伴生类中有一个字段field和一个方法doSomething 。

编译这个文件, 同样生成两个class, 一个TEST.class和一个Test$.class 。 前文已经说过, 这个Test$.class叫做虚构类。

下面先反编译虚构类, 看看加入了伴生类之后, 编译出的虚构类是不是和上一篇博客中的相同。 下面是反编译结果。 (去掉了常量池等冗余的信息)

[java] view
plain
 copy

  1. public final class Test$
  2. SourceFile: "Test.scala"
  3. Scala: length = 0x0
  4. minor version: 0
  5. major version: 50
  6. flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
  7. {
  8. public static final Test$ MODULE$;
  9. flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
  10. private final java.lang.String a;
  11. flags: ACC_PRIVATE, ACC_FINAL
  12. public static {};
  13. flags: ACC_PUBLIC, ACC_STATIC
  14. Code:
  15. stack=1, locals=0, args_size=0
  16. 0: new           #2                  // class Test$
  17. 3: invokespecial #12                 // Method "<init>":()V
  18. 6: return
  19. public java.lang.String a();
  20. flags: ACC_PUBLIC
  21. Code:
  22. stack=1, locals=1, args_size=1
  23. 0: aload_0
  24. 1: getfield      #17                 // Field a:Ljava/lang/String;
  25. 4: areturn
  26. public void printString();
  27. flags: ACC_PUBLIC
  28. Code:
  29. stack=2, locals=1, args_size=1
  30. 0: getstatic     #24                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
  31. 3: aload_0
  32. 4: invokevirtual #26                 // Method a:()Ljava/lang/String;
  33. 7: invokevirtual #30                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
  34. 10: return
  35. private Test$();
  36. flags: ACC_PRIVATE
  37. Code:
  38. stack=2, locals=1, args_size=1
  39. 0: aload_0
  40. 1: invokespecial #31                 // Method java/lang/Object."<init>":()V
  41. 4: aload_0
  42. 5: putstatic     #33                 // Field MODULE$:LTest$;
  43. 8: aload_0
  44. 9: ldc           #35                 // String a string
  45. 11: putfield      #17                 // Field a:Ljava/lang/String;
  46. 14: return
  47. }

可以和上一篇博客中的对比一下, 发现虚构类没有任何变化, 源码中的单例对象中的字段和方法都在虚构类中有相应的对应字段和方法。 并且会为单例对象中的字段生成相应的方法。要说明的重点是:虽然在这个示例中加入了伴生类, 并且伴生类中也有字段和方法, 但是这个字段和方法并没有对应出现在虚构类中。 这也就说明, 虚构类中的信息只和单例对象有关, 单例对象的伴生类不会影响虚构类中的内容。 关于虚构类的实现细节,
可以参阅上一篇博客, 这里不再重复。

下面反编译Test.class 。 反编译结果如下:

[java] view
plain
 copy

  1. public class Test
  2. SourceFile: "Test.scala"
  3. RuntimeVisibleAnnotations:
  4. 0: #6(#7=s#8)
  5. ScalaSig: length = 0x3
  6. 05 00 00
  7. minor version: 0
  8. major version: 50
  9. flags: ACC_PUBLIC, ACC_SUPER
  10. {
  11. private java.lang.String field;
  12. flags: ACC_PRIVATE
  13. public static void printString();
  14. flags: ACC_PUBLIC, ACC_STATIC
  15. Code:
  16. stack=1, locals=0, args_size=0
  17. 0: getstatic     #16                 // Field Test$.MODULE$:LTest$;
  18. 3: invokevirtual #18                 // Method Test$.printString:()V
  19. 6: return
  20. public static java.lang.String a();
  21. flags: ACC_PUBLIC, ACC_STATIC
  22. Code:
  23. stack=1, locals=0, args_size=0
  24. 0: getstatic     #16                 // Field Test$.MODULE$:LTest$;
  25. 3: invokevirtual #22                 // Method Test$.a:()Ljava/lang/String;
  26. 6: areturn
  27. public java.lang.String field();
  28. flags: ACC_PUBLIC
  29. Code:
  30. stack=1, locals=1, args_size=1
  31. 0: aload_0
  32. 1: getfield      #26                 // Field field:Ljava/lang/String;
  33. 4: areturn
  34. public void field_$eq(java.lang.String);
  35. flags: ACC_PUBLIC
  36. Code:
  37. stack=2, locals=2, args_size=2
  38. 0: aload_0
  39. 1: aload_1
  40. 2: putfield      #26                 // Field field:Ljava/lang/String;
  41. 5: return
  42. public void doSomeThing();
  43. flags: ACC_PUBLIC
  44. Code:
  45. stack=2, locals=1, args_size=1
  46. 0: getstatic     #37                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
  47. 3: ldc           #39                 // String do something
  48. 5: invokevirtual #43                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
  49. 8: return
  50. public Test();
  51. flags: ACC_PUBLIC
  52. Code:
  53. stack=2, locals=1, args_size=1
  54. 0: aload_0
  55. 1: invokespecial #46                 // Method java/lang/Object."<init>":()V
  56. 4: aload_0
  57. 5: ldc           #47                 // String field
  58. 7: putfield      #26                 // Field field:Ljava/lang/String;
  59. 10: return
  60. }

在上一篇文章分析单例对象的时候说过, 单例对象中的每个字段或方法, 都对应Test类中的一个静态同名方法。 从上面的反编译结果可以看到, 这些静态方法仍然存在。 这些方法如下:

[java] view
plain
 copy

  1. public static void printString();
  2. public static java.lang.String a();

这些静态方法的行为和上篇博客分析的是一样的。 这里不再重复。

除了这两个静态方法之外, Test类中还存在一些其他字段和方法, 这些字段和方法都是成员方法, 而不是静态的。 这些字段和方法如下:

[java] view
plain
 copy

  1. private java.lang.String field;
  2. public java.lang.String field();
  3. public void field_$eq(java.lang.String);
  4. public void doSomeThing();
  5. public Test();

这些字段和方法是和伴生类中的字段和方法相对应的。 其中会为字段field添加相关方法public java.lang.String field();和public void field_$eq(java.lang.String); 。 为字段添加同名getter方法和xxx_$eq这样的setter方法,
是scalac编译器的默认行为,这个问题在之前的博客 学习Scala:Scala中的字段和方法 中详细讲解过,
这里不再重复。

这里做一下总结:

1 伴生类中定义的字段和方法, 对应同类class类中的成员字段和成员方法;

2 伴生对象中定义的字段和方法, 对应同名类中的静态方法, 所以可以认为Scala中的object关键字是静态的另一种表示方式, 只是scala将这些静态的东西也封装成了对象;

3 伴生对象中定义的字段和方法, 对应虚构类中的成员字段和方法。

4 同名类中的静态方法, 会访问单例的虚构类对象, 将相关的逻辑调用到虚构类中的成员方法中。 由于虚构类是单例的, 所以可以保证伴生对象中的字段都是唯一的。 也就是说虚构类的单例性, 保证了伴生对象(即scala中的object修饰的单例对象)中信息的唯一性。

下面进行验证:

[java] view
plain
 copy

  1. object Main {
  2. def main(args : Array[String]){
  3. var a = Test.a;
  4. var a1 = Test.a;
  5. println("a eq a1 : " + (a eq a1))
  6. }
  7. }

上面的示例中, 访问了两次单例对象的a属性, 并且比较是否是同一个对象, 输出信息如下:

[java] view
plain
 copy

  1. a eq a1 : true

5 伴生对象中的逻辑, 都转移到虚构类中去处理

6 伴生类中的逻辑, 都转移到同名类中的成员方法中去处理。

7 需要注意,伴生类并不是单例的!!! 它仍然可以创建多个对象, 只要在其他地方能够访问到这个伴生类。下面进行验证:

[java] view
plain
 copy

  1. object Main {
  2. def main(args : Array[String]){
  3. var a = new Test
  4. var a1 = new Test
  5. println("a eq a1 : " + (a eq a1))
  6. }
  7. }

打印结果为:

[java] view
plain
 copy

  1. a eq a1 : false

创建的对象不是同一个对象, 所以Test伴生类不是单例的。

8 如何在Scala中使用单例模式呢?我们上面说过, 单例对象中的属性都是永远唯一的, 所以将伴生类中的所有逻辑全部移到单例对象中, 去除伴生类, 让这个单例对象成为孤立对象, 这个孤立对象天然就是单例的。 以本例中的实例代码为例, 将伴生类和伴生对象合并到一起, 去除伴生类, 得到孤立对象如下:

[java] view
plain
 copy

  1. /*class Test{
  2. var field = "field"
  3. def doSomeThing = println("do something")
  4. }
  5. object  Test {
  6. val a = "a string";
  7. def printString = println(a)
  8. }*/
  9. object Test {
  10. var field = "field"
  11. def doSomeThing = println("do something")
  12. val a = "a string";
  13. def printString = println(a)
  14. }

9 如果必须存在伴生类, 怎么保证伴生类是单例的呢? 可以将伴生类的构造器私有, 并且在伴生对象中创建一个伴生类的对象, 这个对象就是唯一的。 代码如下所示:

[java] view
plain
 copy

  1. class Test private {
  2. var field = "field"
  3. def doSomeThing = println("do something")
  4. }
  5. object Test {
  6. val single = new Test
  7. val a = "a string"
  8. def printString = println(a)
  9. }

下面进行验证:

[java] view
plain
 copy

  1. object Main {
  2. def main(args : Array[String]){
  3. var a = Test.single
  4. var a1 = Test.single;
  5. println("a eq a1 : " + (a eq a1))
  6. }
  7. }

打印结果为:

[java] view
plain
 copy

  1. a eq a1 : true

从输出结果可以知道, 两次访问Test.single得到的是同一个对象。 并且这个对象是唯一的, 在外部不能创建伴生类的对象, 因为它的构造器已经私有了, 所以他是单例的。

时间: 2024-10-09 10:07:04

scala中的伴生对象实现原理的相关文章

scala中的孤立对象实现原理

<Scala编程>这本书中, 把孤立对象和伴生对象都叫做单例对象.孤立对象指的是只有一个使用object关键字定义的对象, 伴生对象是指有一个使用object关键字定义的对象, 除此之外还有一个使用class关键字定义的同名类, 这个同名的类叫做伴生类.在Scala中单例对象这个概念多少都会让人迷惑, 按<Scala编程>这本书中的说法, 使用object关键字修饰的对象就叫做单例对象.其实这里的单例和设计模式中的单例模式的概念并不尽相同.在Scala中没有静态的概念, 所有的东西

好程序员大数据学习路线分享scala单列和伴生对象

scala单例 object SingletonDemo { ??def main(args: Array[String]): Unit = { ????val s = SessionFactory ????println(s.getSession) ????println(s.getSession.size) ?//.size得到几个session对象 ??} } object SessionFactory{ ??println("SessionFactory 被执行了") ?? ?

Scala编程之伴生对象

伴生对象是scala中静态的概念 Scala语言是完全面向对象(万物皆对象)的语言,所以并没有静态的操作(即在 Scala中没有静态的概念).但是为了能够和Java语言交互(因为Java中有静态概念),就产生了一种特殊的对象来模拟类对象,我们称之为类的伴生对象.这个类的所有静态内容都可以放置在它的伴生对象中声明和调用. 伴生对象的使用 下面我们通过一个小案例,对伴生对象和伴生类的特点进行探讨 //首先我们创建伴生类class ScalaPerson和伴生对象 object ScalaPerson

Scala学习笔记-伴生对象于孤立对象

Scala-伴生对象于孤立对象 Scala虽然是基于JVM构建的,但与Java还是有很多区别,其实一个重要的区别是Scala比Java更面向对象(纯面向对象),最明显的用例是scala中所有的操作符都是方法(操作符面向的使用者是对象). 伴生对象/孤立对象也是scala作为纯面向对象语言的一种体现. 孤立对象 先看一个例子 object Test{ var a = "helloworld" def helloworld(): Unit = { println("hellowo

scala学习手记15 - 独立对象和伴生对象

上一节中的单例对象MarkerFactory 就是一个独立对象的例子.尽管它管理着Marker类,但是它并没有关联到任何类上. scala也可以创建关联到类上的对象.这样的对象同类共享同一个名字,这样的对象称为伴生对象,对应的类就称为伴生类.在scala里,类和伴生对象没有界限,它们互相可以访问彼此的private 方法和private 属性.下面使用伴生对象重写了Marker: class Marker private(val color: String) { println("Creatin

scala 单例、伴生对象、伴生类

单例:使得对象成为系统中的唯一实例 package scala object single { private var sno:Int = 3; def singlesno()={ sno += 1 sno } } object singleTest { def main(args: Array[String]): Unit = { println(single.singlesno()) println(single.singlesno()) } } 伴生对象: 当单例对象与某个类共享同一个名称

Scala伴生对象

Scala伴生对象 大部分Scala的单例对象不是单独存在的,而是同时存在相同名称的类.此时这个单例对象是"类"的伴生对象,而类则是"单例对象"的伴生类.类与它的伴生对象必须在同一个资源文件内. 例如: case class NumberPair(num1: Int, num2: Int) case class Sum(sumResult: Int) object NumberPair {  implicit def add: NumberPair => Su

聊聊 Scala 的伴生对象及其意义

2019-04-22 关键字:Scala 伴生对象的作用 关于 Scala 伴生对象,比教材更详细的解释. 什么是伴生对象? 教材中关于伴生对象的解释是:实现类似 Java 中那种既有实例成员又有静态成员的类的功能. 为什么上面说它是一种 "功能" 呢?因为要想实现像 Java 中那样的类,光靠一个 Scala 类可不行.在 Scala 中,我们必须: 1. 定义一个 class 并在这里面实现所有的实例成员. 2. 添加一个 object ,这个 object 要与上面的 class

scala学习-类与对象

类 / 对象 [<快学Scala>笔记] 一.类 1.Scala中的类是公有可见性的,且多个类可以包含在同一个源文件中: 1 class Counter{ 2 private var value = 0 //类成员变量必须初始化,否则报错 3 4 def increment(){ //类中的方法默认是公有可见性 5 value += 1 6 } 7 8 def current() = value //对于类中的“取值方法”,在定义时可省略掉括号,直接 def current = value 9