您的位置:首页 > 其它

学习Scala:孤立对象的实现原理

2016-05-31 14:50 295 查看
在关于Scala的第一篇文章 学习Scala:从HelloWorld开始 中, 我们讲述了Scala的HelloWorld程序的执行原理。在Scala中,程序的入口使用孤立对象来实现, 在这篇博客中,
我们讲述了孤立对象是如何实现程序入口的, 不管Scala和Java的语法差别多大, 只要能以一定的方式实现标准的class文件入口类和入口函数, 就能被JVM执行。感兴趣的读者可以移步这篇博客。

在《Scala编程》这本书中, 把孤立对象和伴生对象都叫做单例对象。孤立对象指的是只有一个使用object关键字定义的对象, 伴生对象是指有一个使用object关键字定义的对象, 除此之外还有一个使用class关键字定义的同名类, 这个同名的类叫做伴生类。在Scala中单例对象这个概念多少都会让人迷惑, 按《Scala编程》这本书中的说法, 使用object关键字修饰的对象就叫做单例对象。其实这里的单例和设计模式中的单例模式的概念并不尽相同。在Scala中没有静态的概念, 所有的东西都是面向对象的。其实object单例对象只是对静态的一种封装而已,
在class文件层面中,object单例对象就是用静态(static)来实现的。在本博客中会对这个概念进行详细讲述。除此之外,在本文中还会涉及一重要的概念, 叫做虚构类(synthetic class) 。

关于孤立对象的实现原理, 学习Scala:从HelloWorld开始 这篇文章 已经讲解的差不多了, 本博客可以看做对上一篇博客的补充。

我们还是以简单的例子开始:

[java] view
plain copy







object Test {

val a = "a string";

def printString = println(a)

}

在这个孤立对象上, 定义了一个属性和一个方法。 在编译完成之后, 会看到有两个class文件



也就是说, 这个孤立对象也被编译成一个同名类Test 。 除此之外, 还有一个叫做Test$的类, 这个以$结尾的类就是所谓的虚构类(synthetic class, 《Scala编程》中将之翻译为虚构类) 。

下面使用javap反编译Test.class , 得到如下结果(去掉了常量池等信息):

[java] view
plain copy







public final class Test

SourceFile: "Test.scala"

RuntimeVisibleAnnotations:

0: #6(#7=s#8)

ScalaSig: length = 0x3

05 00 00

minor version: 0

major version: 50

flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER

{

public static void printString();

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=0, args_size=0

0: getstatic #16 // Field Test$.MODULE$:LTest$;

3: invokevirtual #18 // Method Test$.printString:()V

6: return

public static java.lang.String a();

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=0, args_size=0

0: getstatic #16 // Field Test$.MODULE$:LTest$;

3: invokevirtual #22 // Method Test$.a:()Ljava/lang/String;

6: areturn

}

由反编译的结果可以看出, 源码中的属性a对应一个静态的同名方法a(), 源码中的方法printString也对应一个静态的同名方法printString()。 静态方法a()调用Test$类中的静态字段MODULE$的a方法。 静态方法printString()调用Test$类中的静态字段MODULE$的printString方法。 如果用java来描述的话, Test类的逻辑是这样的:

[java] view
plain copy







public final class Test{

public static java.lang.String a(){

return Test$.MODULE$.a()

}

public static void printString(){

Test$.MODULE$.printString()

}

}

下面再看Test类的虚构类Test$的javap反编译结果:

[java] view
plain copy







public final class Test$

SourceFile: "Test.scala"

Scala: length = 0x0

minor version: 0

major version: 50

flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER

{

public static final Test$ MODULE$;

flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

private final java.lang.String a;

flags: ACC_PRIVATE, ACC_FINAL

public static {};

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=0, args_size=0

0: new #2 // class Test$

3: invokespecial #12 // Method "<init>":()V

6: return

public java.lang.String a();

flags: ACC_PUBLIC

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: getfield #17 // Field a:Ljava/lang/String;

4: areturn

public void printString();

flags: ACC_PUBLIC

Code:

stack=2, locals=1, args_size=1

0: getstatic #24 // Field scala/Predef$.MODULE$:Lscala/Predef$;

3: aload_0

4: invokevirtual #26 // Method a:()Ljava/lang/String;

7: invokevirtual #30 // Method scala/Predef$.println:(Ljava/lang/Object;)V

10: return

private Test$();

flags: ACC_PRIVATE

Code:

stack=2, locals=1, args_size=1

0: aload_0

1: invokespecial #31 // Method java/lang/Object."<init>":()V

4: aload_0

5: putstatic #33 // Field MODULE$:LTest$;

8: aload_0

9: ldc #35 // String a string

11: putfield #17 // Field a:Ljava/lang/String;

14: return

}

首先看一下这个类里的内容。

首先, 该类中有一个常量字段MODULE$, 它的类型就是当前的虚构类Test$ 。

[java] view
plain copy







public static final Test$ MODULE$;

flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

编译器在Test$中默认添加了静态初始化方法, 用于对静态字段MODULE$初始化:

[java] view
plain copy







public static {};

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=0, args_size=0

0: new #2 // class Test$

3: invokespecial #12 // Method "<init>":()V

6: return

源码中的字段a在Test$中对应一个非静态的字段a , 由于源码中的a是val的, 所以在Test$中对应的a字段是final的

[java] view
plain copy







private final java.lang.String a;

flags: ACC_PRIVATE, ACC_FINAL

在Test$中还有一个成员方法a()与字段a对应, 这个方法的逻辑是返回a的值

[java] view
plain copy







public java.lang.String a();

flags: ACC_PUBLIC

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: getfield #17 // Field a:Ljava/lang/String;

4: areturn

源码中的方法printString对应Test$中的printString方法。 这个方法的逻辑是调用方法a()获取字段a的值, 并打印a的值。

[java] view
plain copy







public void printString();

flags: ACC_PUBLIC

Code:

stack=2, locals=1, args_size=1

0: getstatic #24 // Field scala/Predef$.MODULE$:Lscala/Predef$;

3: aload_0

4: invokevirtual #26 // Method a:()Ljava/lang/String;

7: invokevirtual #30 // Method scala/Predef$.println:(Ljava/lang/Object;)V

10: return

此外, 编译器在Test$中还加入默认的构造方法, 不过这个构造方法是私有的。 无法为外部调用。如下:

[java] view
plain copy







private Test$();

flags: ACC_PRIVATE

Code:

stack=2, locals=1, args_size=1

0: aload_0

1: invokespecial #31 // Method java/lang/Object."<init>":()V

4: aload_0

5: putstatic #33 // Field MODULE$:LTest$;

8: aload_0

9: ldc #35 // String a string

11: putfield #17 // Field a:Ljava/lang/String;

14: return

如果用java代码描述的话,Test$的逻辑是这样的:

[java] view
plain copy







public final class Test${

public static final Test$ MODULE$ = new Test$();

private final String a = "a string";

public String a(){

return a;

}

public void printString(){

println(a());

}

private Test$(){}

}

由此可见, 这个虚构类Test$是单例的。 一方面, 这个类是编译器默认生成的,在Scala代码中无法访问到。 另一方面, Test$构造器私有了, 只在内部创建了一个对象赋给了静态引用MODULE$ 。

所以, 在Scala里面称用object关键字修饰的对象是单例对象, 在实现的角度上看, 并不是十分确切。 虽然称之为对象, 但是编译器确实为他生成了一个类, 如上面例子中的object Test , 编译器确实生成了类Test。 但是这个类中只有静态方法, 即使是一个Scala中的字段, 也对应一个静态方法, 如上例中的字段a 。 这个类中的静态方法会访问虚构类Test$中的静态成员Test$ MODULE$ ,使用这个对象可以调用Test$中的其他成员方法,Test$中的成员和源码中的成员相对应,
只是会为源码中的字段添加同名方法。 主要的处理逻辑实际上是在虚构类Test$中完成的, Test类只是作为一个入口。

下面是看一下Scala是如何实现对单例对象的调用的。 首先写一个Scala的入口类:

[java] view
plain copy







object Main {

//scala main

def main(args : Array[String]){

Test.printString

}

}

相同的原理, 入口类Main也是单例对象, 实现原理和Test是相同的。 大部分的逻辑都在虚构类Main$中的成员方法main中实现的。反编译 Main$后的结果如下:

[java] view
plain copy







public final class Main$

SourceFile: "Main.scala"

Scala: length = 0x0

minor version: 0

major version: 50

flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER

{

public static final Main$ MODULE$;

flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

public static {};

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=0, args_size=0

0: new #2 // class Main$

3: invokespecial #12 // Method "<init>":()V

6: return

public void main(java.lang.String[]);

flags: ACC_PUBLIC

Code:

stack=1, locals=2, args_size=2

0: getstatic #19 // Field Test$.MODULE$:LTest$;

3: invokevirtual #22 // Method Test$.printString:()V

6: return

private Main$();

flags: ACC_PRIVATE

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: invokespecial #26 // Method java/lang/Object."<init>":()V

4: aload_0

5: putstatic #28 // Field MODULE$:LMain$;

8: return

}

用Java代码实现如下:

[java] view
plain copy







public final class Main${

public static final Main$ MODULE$ = new Main$();

public void main(String[] args){

Test$.MODULE$.printString();

}

private Main$(){}

}

由此可见, 在Main$中的成员方法main中, 直接调用了Test$.MODULE$.printString()方法, 而绕过了Test类, 这也是合理的, 因为只有Test$才处理相关逻辑。

而Main.class用java代码表示如下:

[java] view
plain copy







public final class Main{

public static void main(String[] args){

Main$.MODULE$.main(args);

}

}

做一下总结:

Main.class提供JVM的入口函数, 在入口函数中调用Main$的成员方法main, 而Main$的成员方法main又调用了Test$的成员方法printString来处理相关逻辑, 即打印字符串。

单例对象的调用方式如下图所示:



在关于Scala的第一篇文章 学习Scala:从HelloWorld开始 中, 我们讲述了Scala的HelloWorld程序的执行原理。在Scala中,程序的入口使用孤立对象来实现, 在这篇博客中,
我们讲述了孤立对象是如何实现程序入口的, 不管Scala和Java的语法差别多大, 只要能以一定的方式实现标准的class文件入口类和入口函数, 就能被JVM执行。感兴趣的读者可以移步这篇博客。

在《Scala编程》这本书中, 把孤立对象和伴生对象都叫做单例对象。孤立对象指的是只有一个使用object关键字定义的对象, 伴生对象是指有一个使用object关键字定义的对象, 除此之外还有一个使用class关键字定义的同名类, 这个同名的类叫做伴生类。在Scala中单例对象这个概念多少都会让人迷惑, 按《Scala编程》这本书中的说法, 使用object关键字修饰的对象就叫做单例对象。其实这里的单例和设计模式中的单例模式的概念并不尽相同。在Scala中没有静态的概念, 所有的东西都是面向对象的。其实object单例对象只是对静态的一种封装而已,
在class文件层面中,object单例对象就是用静态(static)来实现的。在本博客中会对这个概念进行详细讲述。除此之外,在本文中还会涉及一重要的概念, 叫做虚构类(synthetic class) 。

关于孤立对象的实现原理, 学习Scala:从HelloWorld开始 这篇文章 已经讲解的差不多了, 本博客可以看做对上一篇博客的补充。

我们还是以简单的例子开始:

[java] view
plain copy







object Test {

val a = "a string";

def printString = println(a)

}

在这个孤立对象上, 定义了一个属性和一个方法。 在编译完成之后, 会看到有两个class文件



也就是说, 这个孤立对象也被编译成一个同名类Test 。 除此之外, 还有一个叫做Test$的类, 这个以$结尾的类就是所谓的虚构类(synthetic class, 《Scala编程》中将之翻译为虚构类) 。

下面使用javap反编译Test.class , 得到如下结果(去掉了常量池等信息):

[java] view
plain copy







public final class Test

SourceFile: "Test.scala"

RuntimeVisibleAnnotations:

0: #6(#7=s#8)

ScalaSig: length = 0x3

05 00 00

minor version: 0

major version: 50

flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER

{

public static void printString();

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=0, args_size=0

0: getstatic #16 // Field Test$.MODULE$:LTest$;

3: invokevirtual #18 // Method Test$.printString:()V

6: return

public static java.lang.String a();

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=0, args_size=0

0: getstatic #16 // Field Test$.MODULE$:LTest$;

3: invokevirtual #22 // Method Test$.a:()Ljava/lang/String;

6: areturn

}

由反编译的结果可以看出, 源码中的属性a对应一个静态的同名方法a(), 源码中的方法printString也对应一个静态的同名方法printString()。 静态方法a()调用Test$类中的静态字段MODULE$的a方法。 静态方法printString()调用Test$类中的静态字段MODULE$的printString方法。 如果用java来描述的话, Test类的逻辑是这样的:

[java] view
plain copy







public final class Test{

public static java.lang.String a(){

return Test$.MODULE$.a()

}

public static void printString(){

Test$.MODULE$.printString()

}

}

下面再看Test类的虚构类Test$的javap反编译结果:

[java] view
plain copy







public final class Test$

SourceFile: "Test.scala"

Scala: length = 0x0

minor version: 0

major version: 50

flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER

{

public static final Test$ MODULE$;

flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

private final java.lang.String a;

flags: ACC_PRIVATE, ACC_FINAL

public static {};

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=0, args_size=0

0: new #2 // class Test$

3: invokespecial #12 // Method "<init>":()V

6: return

public java.lang.String a();

flags: ACC_PUBLIC

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: getfield #17 // Field a:Ljava/lang/String;

4: areturn

public void printString();

flags: ACC_PUBLIC

Code:

stack=2, locals=1, args_size=1

0: getstatic #24 // Field scala/Predef$.MODULE$:Lscala/Predef$;

3: aload_0

4: invokevirtual #26 // Method a:()Ljava/lang/String;

7: invokevirtual #30 // Method scala/Predef$.println:(Ljava/lang/Object;)V

10: return

private Test$();

flags: ACC_PRIVATE

Code:

stack=2, locals=1, args_size=1

0: aload_0

1: invokespecial #31 // Method java/lang/Object."<init>":()V

4: aload_0

5: putstatic #33 // Field MODULE$:LTest$;

8: aload_0

9: ldc #35 // String a string

11: putfield #17 // Field a:Ljava/lang/String;

14: return

}

首先看一下这个类里的内容。

首先, 该类中有一个常量字段MODULE$, 它的类型就是当前的虚构类Test$ 。

[java] view
plain copy







public static final Test$ MODULE$;

flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

编译器在Test$中默认添加了静态初始化方法, 用于对静态字段MODULE$初始化:

[java] view
plain copy







public static {};

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=0, args_size=0

0: new #2 // class Test$

3: invokespecial #12 // Method "<init>":()V

6: return

源码中的字段a在Test$中对应一个非静态的字段a , 由于源码中的a是val的, 所以在Test$中对应的a字段是final的

[java] view
plain copy







private final java.lang.String a;

flags: ACC_PRIVATE, ACC_FINAL

在Test$中还有一个成员方法a()与字段a对应, 这个方法的逻辑是返回a的值

[java] view
plain copy







public java.lang.String a();

flags: ACC_PUBLIC

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: getfield #17 // Field a:Ljava/lang/String;

4: areturn

源码中的方法printString对应Test$中的printString方法。 这个方法的逻辑是调用方法a()获取字段a的值, 并打印a的值。

[java] view
plain copy







public void printString();

flags: ACC_PUBLIC

Code:

stack=2, locals=1, args_size=1

0: getstatic #24 // Field scala/Predef$.MODULE$:Lscala/Predef$;

3: aload_0

4: invokevirtual #26 // Method a:()Ljava/lang/String;

7: invokevirtual #30 // Method scala/Predef$.println:(Ljava/lang/Object;)V

10: return

此外, 编译器在Test$中还加入默认的构造方法, 不过这个构造方法是私有的。 无法为外部调用。如下:

[java] view
plain copy







private Test$();

flags: ACC_PRIVATE

Code:

stack=2, locals=1, args_size=1

0: aload_0

1: invokespecial #31 // Method java/lang/Object."<init>":()V

4: aload_0

5: putstatic #33 // Field MODULE$:LTest$;

8: aload_0

9: ldc #35 // String a string

11: putfield #17 // Field a:Ljava/lang/String;

14: return

如果用java代码描述的话,Test$的逻辑是这样的:

[java] view
plain copy







public final class Test${

public static final Test$ MODULE$ = new Test$();

private final String a = "a string";

public String a(){

return a;

}

public void printString(){

println(a());

}

private Test$(){}

}

由此可见, 这个虚构类Test$是单例的。 一方面, 这个类是编译器默认生成的,在Scala代码中无法访问到。 另一方面, Test$构造器私有了, 只在内部创建了一个对象赋给了静态引用MODULE$ 。

所以, 在Scala里面称用object关键字修饰的对象是单例对象, 在实现的角度上看, 并不是十分确切。 虽然称之为对象, 但是编译器确实为他生成了一个类, 如上面例子中的object Test , 编译器确实生成了类Test。 但是这个类中只有静态方法, 即使是一个Scala中的字段, 也对应一个静态方法, 如上例中的字段a 。 这个类中的静态方法会访问虚构类Test$中的静态成员Test$ MODULE$ ,使用这个对象可以调用Test$中的其他成员方法,Test$中的成员和源码中的成员相对应,
只是会为源码中的字段添加同名方法。 主要的处理逻辑实际上是在虚构类Test$中完成的, Test类只是作为一个入口。

下面是看一下Scala是如何实现对单例对象的调用的。 首先写一个Scala的入口类:

[java] view
plain copy







object Main {

//scala main

def main(args : Array[String]){

Test.printString

}

}

相同的原理, 入口类Main也是单例对象, 实现原理和Test是相同的。 大部分的逻辑都在虚构类Main$中的成员方法main中实现的。反编译 Main$后的结果如下:

[java] view
plain copy







public final class Main$

SourceFile: "Main.scala"

Scala: length = 0x0

minor version: 0

major version: 50

flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER

{

public static final Main$ MODULE$;

flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

public static {};

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=0, args_size=0

0: new #2 // class Main$

3: invokespecial #12 // Method "<init>":()V

6: return

public void main(java.lang.String[]);

flags: ACC_PUBLIC

Code:

stack=1, locals=2, args_size=2

0: getstatic #19 // Field Test$.MODULE$:LTest$;

3: invokevirtual #22 // Method Test$.printString:()V

6: return

private Main$();

flags: ACC_PRIVATE

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: invokespecial #26 // Method java/lang/Object."<init>":()V

4: aload_0

5: putstatic #28 // Field MODULE$:LMain$;

8: return

}

用Java代码实现如下:

[java] view
plain copy







public final class Main${

public static final Main$ MODULE$ = new Main$();

public void main(String[] args){

Test$.MODULE$.printString();

}

private Main$(){}

}

由此可见, 在Main$中的成员方法main中, 直接调用了Test$.MODULE$.printString()方法, 而绕过了Test类, 这也是合理的, 因为只有Test$才处理相关逻辑。

而Main.class用java代码表示如下:

[java] view
plain copy







public final class Main{

public static void main(String[] args){

Main$.MODULE$.main(args);

}

}

做一下总结:

Main.class提供JVM的入口函数, 在入口函数中调用Main$的成员方法main, 而Main$的成员方法main又调用了Test$的成员方法printString来处理相关逻辑, 即打印字符串。

单例对象的调用方式如下图所示:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: