7.3 修改DVM

7.3.1 增加指令

由于这次引入了数组,因此在DVM中也要增加相应的指令,增加的指令如表7-1 [2]所示。

表7-1 随着引入数组增加的指令

figure_0232_0064

在指令中出现了“object型”的概念。它是在之前只包含了字符串的引用类型的基础上又增加了数组,是由字符串和数组组成的类型。

随着上述改变,除了专门处理字符串的操作(如字符串比较等),之前的 push_static_string等指令都要重命名为 push_static_object等了。

表7-1的指令中,我想必须要特别说明一下 new_array。

在表7-1中提到了“操作数short代表的类型”,这个操作数是指这次在 DVM_Executable中新增的 DVM_TypeSpecifier数组的下标(代码清单7-2)。

代码清单7-2 DVM_Executable (book_ver.0.2) 

struct DVM_Executable_tag {

int    constant_pool_count;

DVM_ConstantPool *constant_pool;

int    global_variable_count;

DVM_Variable  *global_variable;

int    function_count;

DVM_Function  *function;

int    type_specifier_count; ←新增

DVM_TypeSpecifier *type_specifier; ←新增

int    code_size;

DVM_Byte   *code;

int    line_number_size;

DVM_LineNumber  *line_number;

int    need_stack_size;

};

DVM_TypeSpecifier结构体在book_ver.0.1时就已经存在了,它和TypeSpecifier结构体保存着同样的信息。

例如,使用 new int[5][3]创建一个数组, new_array的操作数将被指定为保存着 int[][]类型信息的 DVM_TypeSpecifier的下标。

这样一来,只要知道对应的TypeSpecifier 就能够知道数组的维数, int[] [] 型的数组也可以像 new int[5][] 这样,在代码运行过程中再创建另外一维。实际创建的维数(这里是1)使用另外一个byte型操作数传递指令。另外,使用代码 a = new int[5][];创建的数组和Java一样, a[0]~a[4]被初始化为 null。

补充知识 创建Java的数组常量

如表7-1所示,在DVM中, new_array_literal_int等创建常量的指令,会先将组成数组的值入栈,再利用已经在栈上的值创建数组。但是,JVM就没有与此对应的指令。那么,Java中是如何通过构造函数创建的数组或者使用 new int {1,2, 3}这样的代码创建数组的呢?让我们使用 javap来看一下。

最初的代码:

class Test {

public static void main(String[] args){

int[] a = {1, 2, 3, 4, 5};

}

}

javap 的结果(只截取了指令部分): 

0: iconst_5

1: newarray int

3: dup

4: iconst_0

5: iconst_1

6: iastore

7: dup

8: iconst_1

9: iconst_2

10: iastore

11: dup

12: iconst_2

13: iconst_3

14: iastore

15: dup

16: iconst_3

17: iconst_4

18: iastore

19: dup

20: iconst_4

21: iconst_5

22: iastore

23: astore_1

24: return

也就是说,相当于下面这段代码。

int[] a = new int[5];

a[0] = 1;

a[1] = 2;

a[2] = 3;

a[3] = 4;

a[4] = 5;

在我看来,生成字节码的体积太大了。在Java中,与一个方法对应的字节码是有大小限制的(Diksam也一样),所以自动生成代码的时候(也许还有其他情况)可能会引起问题。

补充知识 C语言中数组的初始化

Diksam也好,Java也好,数组常量以及利用构造函数创建的数组,它们的内容都是在“运行时”决定的。所以,在下面这段代码中:

int[] a = {b * 10, func()};

从这段初始化的程序可以看出,数组元素可能只在运行时才能决定其值的表达式。

对此,在C中利用初始化程序初始化数组的时候,元素的内容必须是常量表达式。

因为有了这个限制,在编译时可以预先创建数组的内存映像, static变量开始执行、自动变量 [3]进入函数时,可以利用事先创建的内存映像进行初始化。

7.3.2 对象

在book_ver.0.1中可以称为对象的只有字符串,现在在 DVM_Object中增加了数组成员,以对应这次新增的数组概念。

struct DVM_Object_tag {

ObjectType type;

unsigned int marked:1;

union {

DVM_String string;

DVM_Array array; ←新增

} u;

struct DVM_Object_tag *prev;

struct DVM_Object_tag *next;

};

DVM_Array的内容如下所示。

typedef enum {

INT_ARRAY = 1,

DOUBLE_ARRAY,

OBJECT_ARRAY,

} ArrayType;

struct DVM_Array_tag {

ArrayType type;

int  size;

int  alloc_size;

union {

int  *int_array;

double  *double_array;

DVM_Object **object;

} u;

};

在crowbar中,数组是“ CRB_Value的数组 [4]”。这次也一样,因为有 DVM_Value共用体,数组也可以表现为数组。但是,由于Diksam是静态语言,因此数组的类型是静态决定的。绝对不可能把 double加入到 int的数组中。

这么说的话,int 的数组使用“ sizeof(int)× 元素数”就可以毫无浪费地创建内存空间,即使是传递给C的内置例程处理起来也很舒适。因此,枚举类型 ArrayType 中的每个对象都表示不同数组元素的类型。 ArrayType 没有必要对应Diksam中的所有类型。例如字符串的数组,或者是数组的数组,这些都是 OBJECT_ARRAY。数组的类型在编译时决定,因此运行时在这里没有必要保存严格的类型。现在的情况是,数组的类型信息只有GC用到了。

补充知识 ArrayStoreException

前面写到,在Diksam中既有字符串数组也有数组的数组,数组的对象中只保存了“ OBJECT_ARRAY”这一个信息。与此相对,在Java中,数组对象中保存着完整的类型信息。这样的区别是基于以下两点原因。

Diksam中还不存在类和继承,但是在Java中存在。

在Java中,当 A是 B 的子类时, A[] 也自动地成为了 B[] 的子类。 

例如有一个表示图形的类 Shape,有两个继承它的子类 Line和 Circle。这时在Java中,可以把 Line的数组赋值给 Shape[]型的变量。这种设计乍看是挺方便的,实际上问题重重。请思考如下这个代码片段。

1: Line[] lines = new Line[10];

2: Shape[] shapes = lines;

3: shapes[3] = new Circle();

4: lines[3]. startPoint = new Point(x, y);

第1行当然是合法的。第2行也一样, Line[]是 Shape[]的子类,因此在Java中也是合法的。第3行,因为Circle也是Shape的子类,所以Java在编译时并不会报错(更确切地说是报不出错)。

接下来的第4行就悲剧了。 shapes和 lines指向同一个数组,因此 lines[3]也就是 shapes[3],它在第3行被赋了一个 Circle 对象的值。但是,在第4行的时候又要引用 Line 的起点(startPoint), Circle 中并没有 startPoint,因此这行代码不能被执行。但是,编译器却始终认为 line[3]肯定是 Line,因此编译时不会出现报错。 正因如此,在 Java 中,执行到第3行代码时会发生运行时的异常, ArrayStoreException。

只有在运行时掌握“这个数组在变量声明上是 Shape[],但是它实际上却是 Line[]”,才能在实现时抛出上述异常。因此,在Java中,必须将完整的类型信息保存在数组的对象中。

我认为这是一个不良的设计。既然是静态语言,就应该在编译时完成类型检查,在运行时抛出异常不是很奇怪的吗?总之,我认为Java的“ A是 B的子类时, A[]也自动地成为了 B[]的子类”这个规则是错误的。

Diksam将在下一章引入类的概念,但是没有建立上述规则,因此也没有必要在数组中保存严格的类型信息。

7.3.3 增加null

由于数组和字符串都是引用类型,因此增加了 null。

随之改变的是,以前字符串变量的初始值是空字符串,现在变成了 null。

关于 null的规则如下所示。

1. 字符串类型、数组类型的变量可以赋值为 null。 

2. 字符串类型与值为 null的变量用 +连接的话, null会转换为字符串" null"。 

3. 字符串类型与常量 null用 +连接的话, null会转换为字符串" null"。

4. 字符串类型与数组类型可以和 null进行比较。 

7.3.4 哎!还缺点什么吧?

这次引入了数组的概念,如果是了解当今编程语言的人肯定在期待着下面这些功能。既然引入了数组的概念,怎么能没有它们呢?

没有知道数组大小的(array.size()或者 array.length等)方法吗?

没有动态增加数组元素(例如 array.add(5))的方法吗?

不能把数组内容直接输出(例如 print("array.." + array))吗?

这些大概在当今的编程语言中都能实现(有些语言会把数组理所当然地输出为地址或者哈希值),说起来在crowbar中也实现了,但是这次却搁置起来了。这是因为考虑到这些功能最终都归结为“方法”,因此还是和类一起制作更为恰当。

所以,下一章将要对类进行处理。

注 释

[1]. 因为数组的下标是从0开始的,所以这里用一般的计数方法取得的应该是第6个元素。

[2]. 请参考附录C中的范例阅读本表。

[3]. 一般情况下可以看作是局部变量。——译者注

[4]. 这个数组值必须是双向链表。——译者注