Scala入门,以Java程序员的视角(三):孤立对象和伴生对象

孤立对象和伴生对象都是单例对象,这是Scala和Java语法很大差别的地方,本篇将对这两个概念进行深入研究。

孤立对象

解析前的准备工作

先写一个最简单的例子:

1
2
3
4
5
6
7
8
object Testdemo01 {
val a = 3
def add(x: Int) = x * 2
def main(args: Array[String]): Unit = {
add(a)
}
}

这段代码编译后会产生两个.class文件,首先反编译Testdemo01.class(使用javap -c -p会省略常量池,常量池显现可以采用javap -c -v),得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public final class scala.C2.Testdemo01 {
public static void main(java.lang.String[]);
Code:
0: getstatic #16 // Field scala/C2/Testdemo01$.MODULE$:Lscala/C2/Testdemo01$;
3: aload_0
4: invokevirtual #18 // Method scala/C2/Testdemo01$.main:([Ljava/lang/String;)V
7: return
public static int add(int);
Code:
0: getstatic #16 // Field scala/C2/Testdemo01$.MODULE$:Lscala/C2/Testdemo01$;
3: iload_0
4: invokevirtual #22 // Method scala/C2/Testdemo01$.add:(I)I
7: ireturn
public static int a();
Code:
0: getstatic #16 // Field scala/C2/Testdemo01$.MODULE$:Lscala/C2/Testdemo01$;
3: invokevirtual #26 // Method scala/C2/Testdemo01$.a:()I
6: ireturn
}

接下来反编译Testdemo01$.class,得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public final class scala.C2.Testdemo01$ {
public static scala.C2.Testdemo01$ MODULE$;
private final int a;
public static {};
Code:
0: new #2 // class scala/C2/Testdemo01$
3: invokespecial #14 // Method "<init>":()V
6: return
public int a();
Code:
0: aload_0
1: getfield #17 // Field a:I
4: ireturn
public int add(int);
Code:
0: iload_1
1: iconst_2
2: imul
3: ireturn
public void main(java.lang.String[]);
Code:
0: aload_0
1: aload_0
2: invokevirtual #26 // Method a:()I
5: invokevirtual #28 // Method add:(I)I
8: pop
9: return
private scala.C2.Testdemo01$();
Code:
0: aload_0
1: invokespecial #30 // Method java/lang/Object."<init>":()V
4: aload_0
5: putstatic #32 // Field MODULE$:Lscala/C2/Testdemo01$;
8: aload_0
9: iconst_3
10: putfield #17 // Field a:I
13: return
}

字节码解析

为了方便描述,将反编译Testdemo01.class的内容称为为A,将反编译Testdemo01$.class的内容称为B,将改动源码并反编译后的相异部分称为B_change;

从内容上来看,作为程序执行唯一入口的public static void main方法出现在A,所以Java虚拟机主要是通过对Testdemo01.class解释、编译来执行程序的。

A的public static void main方法

  • getstatic #16:访问类字段,将其压入操作数栈的栈顶;
    这里的类就是Testdemo01$,而类字段就是它的一个实例MODULE$;
    
  • aload_0:将局部变量表索引为0的位置保存的引用型数值复制一份到操作数栈的栈顶
    A的main是静态方法,所以局部变量表索引为0的位置存放传入参数java.lang.String[]的字符串数组引用;
    
  • invokevirtual #18:先确定从操作数栈顶开始索引为方法显式参数个数值的元素,然后找到该元素所指向的对象的实际类型,记作C,然后查询并验证C中简单名和描述符都match的方法,如果失败则根据类的继承关系从下往上顺序查询,直到成功或失败,整个过程中栈顶的引用元素和方法的参数也随着命令的执行从栈顶推出
    [《深入理解Java虚拟机(第2版)》](https://book.douban.com/subject/24722612/)写道的“找到操作数栈顶的第一个元素所指向的对象的实际类型记作C”有误。回到原例,invokevirtual会找到getstatic得到的对象的实际类型,于是找到Testdemo01$并执行它的main方法;
    

Scala编译出的Testdemo01类下的方法都是静态方法,和上述中的main方法类似,这里就不多言了,可以发现这些静态方法全部封装了反编译出的Testdemo01$类的方法。

B的public void main方法

  • aload_0:将局部变量表索引为0的位置保存的引用型数值复制一份到操作数栈的栈顶
    B的main不是静态方法,所以索引为0的位置保存的是this,这里将其复制到栈顶;
    
  • aload_0:重复上一步的工作
    执行两次该字节码指令是因为下面会相继调用两个方法,每个方法调用默认需要传入参数this;
    
  • invokevirtual #26:原理同前文中的invokevirtual #18
    这里相当于执行this.a(),并将结果返回main方法的操作数栈;
    
  • invokevirtual #28:同上
    这里相当于执行this.add(x),这里的x是上一步this.a()返回的值;
    

孤立对象总结

现在一切都明了了,Scala将object Testdemo01{…}编译成Testdemo01类和Testdemo01$类,践行的是《Effective Java》中提到的静态工厂方法,只不过在Scala里,整个过程分配在了两个类中;

伪代码类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public final class TestDemo01 {
public static void main(String args[]) {
MODULE$.main(args);
}
public static int add(int a) {
return MODULE$.add(a);
}
public static int a() {
return MODULE$.a();
}
}
final class Testdemo01$ {
private final int a;
public static Testdemo01$ MODULE$ = new Testdemo01$();
private Testdemo01$() {
a = 3;
}
public void main(String args[]) {
add(a());
}
public int a() {
return a;
}
public int add(int a) {
return a * 2;
}
}

伴生对象

伴生对象的产生,是在之前的基础上,于同一文件中添加一个同名class。由于之前的分析,当知道这一规则后,本人多少能猜测到一些Scala语言设计者的设计意图,因为之前Testdemo01类都是静态方法,如若单独添加一个同名Class应该是相当于可以添加独立的字段和方法,下面印证一下。

在前文的代码基础上添加一个同名class:

1
2
3
4
5
6
7
8
class Testdemo01 {
val s01 = "chenchen"
var s02 = "guoguo"
def newAdd(b: Int) = {
b + 1
}
}

之前反编译得到的Testdemo01$类没有任何变化,变化的是反编译产生的Testdemo01类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class scala.C2.Testdemo01 {
private final java.lang.String s01;
private java.lang.String s02;
public static void main(java.lang.String[]);
Code:
0: getstatic #19 // Field scala/C2/Testdemo01$.MODULE$:Lscala/C2/Testdemo01$;
3: aload_0
4: invokevirtual #21 // Method scala/C2/Testdemo01$.main:([Ljava/lang/String;)V
7: return
public static int add(int);
Code:
0: getstatic #19 // Field scala/C2/Testdemo01$.MODULE$:Lscala/C2/Testdemo01$;
3: iload_0
4: invokevirtual #25 // Method scala/C2/Testdemo01$.add:(I)I
7: ireturn
public static int a();
Code:
0: getstatic #19 // Field scala/C2/Testdemo01$.MODULE$:Lscala/C2/Testdemo01$;
3: invokevirtual #29 // Method scala/C2/Testdemo01$.a:()I
6: ireturn
public java.lang.String s01();
Code:
0: aload_0
1: getfield #32 // Field s01:Ljava/lang/String;
4: areturn
public java.lang.String s02();
Code:
0: aload_0
1: getfield #36 // Field s02:Ljava/lang/String;
4: areturn
public void s02_$eq(java.lang.String);
Code:
0: aload_0
1: aload_1
2: putfield #36 // Field s02:Ljava/lang/String;
5: return
public int newAdd(int);
Code:
0: iload_1
1: iconst_1
2: iadd
3: ireturn
public scala.C2.Testdemo01();
Code:
0: aload_0
1: invokespecial #46 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #48 // String chenchen
7: putfield #32 // Field s01:Ljava/lang/String;
10: aload_0
11: ldc #50 // String guoguo
13: putfield #36 // Field s02:Ljava/lang/String;
16: return
}

main、add、a方法之后就是新增的方法,它们是s01、s02、s02_$eq、newAdd以及Testdemo01方法。其中s01、s02是程序隐藏的get方法,s02_$eq是set方法,成功印证了我的猜测;

从字节码可以看出,首先编译的是object存在的部分,如果在class中间添加一个和object签名相同的方法,那么object的这个方法将被覆盖为class的方法,然后被编译,限于篇幅实验略;

小结

本篇主要探讨的是孤立对象和伴生对象,Scala号称没有静态方法域,但通过分析可以很清楚的直到,孤立对象和伴生对象底层是通过静态工厂方法实现的。