本文是这个新系列文章的第一篇,该系列文章将讨论我称之为 Java 编程的动态性的一系 列主题。这些主题的范围从 Java 二进制类文件格式的基本结构,以及使用反射进行运行时 元数据访问,一直到在运行时修改和构造新类。贯穿整篇文章的公共线索是这样一种思想: 在 Java 平台上编程要比使用直接编译成本机代码的语言更具动态性。如果您理解了这些动 态方面,就可以使用 Java 编程完成那些在任何其它主流编程语言中不能完成的事情。
本文中,我将讨论一些基本概念,它们是这些 Java 平台动态特性的基础。这些概念的核 心是用于表示 Java 类的二进制格式,包括这些类装入到 JVM 时所发生的情况。本文不仅是 本系列其余几篇文章的基础,而且还演示了开发人员在使用 Java 平台时碰到的一些非常实 际的问题。
用二进制表示的类
使用 Java 语言的开发人员在用编译器编译他们的源代码时,通常不必关心对这些源代码 做了些什么这样的细节。但是本系列文章中,我将讨论从源代码到执行程序所涉及的许多幕 后细节,因此我将首先探讨由编译器生成的二进制类。
二进制类格式实际上是由 JVM 规范定义的。通常这些类表示是由编译器从 Java 语言源 代码生成的,而且它们通常存储在扩展名为 .class 的文件中。但是,这些特性都无关紧要 。已经开发了可以使用 Java 二进制类格式的其它一些编程语言,而且出于某些目的,还构 建了新的类表示,并被立即装入到运行中的 JVM。就 JVM 而言,重要的部分不是源代码以及 如何存储源代码,而是格式本身。
那么这个类格式实际看上去是什么样呢?清单 1 提供了一个(非常)简短的类的源代码 ,还附带了由编译器输出的类文件的部分十六进制显示:
清单 1. Hello.java 的源代码和(部分)二进制类文件
public class Hello
{
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
0000: cafe babe 0000 002e 001a 0a00 0600 0c09 ................
0010: 000d 000e 0800 0f0a 0010 0011 0700 1207 ................
0020: 0013 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()
0030: 5601 0004 436f 6465 0100 046d 6169 6e01 V...Code...main.
0040: 0016 285b 4c6a 6176 612f 6c61 6e67 2f53 ..([Ljava/lang/S
0050: 7472 696e 673b 2956 0c00 0700 0807 0014 tring;)V........
0060: 0c00 1500 1601 000d 4865 6c6c 6f2c 2057 ........Hello, W
0070: 6f72 6c64 2107 0017 0c00 1800 1901 0005 orld!...........
0080: 4865 6c6c 6f01 0010 6a61 7661 2f6c 616e Hello...java/lan
0090: 672f 4f62 6a65 6374 0100 106a 6176 612f g/Object...java/
00a0: 6c61 6e67 2f53 7973 7465 6d01 0003 6f75 lang/System...ou
...
二进制类文件的内幕
清单 1 显示的二进制类表示中首先是“cafe babe”特征符,它标识 Java 二进制类格式 (并顺便作为一个永久的 ― 但在很大程度上未被认识到的 ― 礼物送给努力工作的 barista,他们本着开发人员所具备的精神构建 Java 平台)。这个特征符恰好是一种验证一 个数据块 确实声明成 Java 类格式的一个实例的简单方法。任何 Java 二进制类(甚至是文 件系统中没有出现的类)都需要以这四个字节作为开始。
该数据的其余部分不太吸引人。该特征符之后是一对类格式版本号(本例中,是由 1.4.1 javac 生成的次版本 0 和主版本 46 ― 用十六进制表示就是 0x2e),接着是常量池中项的 总数。项总数(本例中,是 26,或 0x001a)后面是实际的常量池数据。这里放着类定义所 用的所有常量。它包括类名和方法名、特征符以及字符串(您可以在十六进制转储右侧的文 本解释中识别它们),还有各种二进制值。
常量池中各项的长度是可变的,每项的第一个字节标识项的类型以及对它解码的方式。这 里我不详细探究所有这些内容的细节,如果感兴趣,有许多可用的的参考资料,从实际的 JVM 规范开始。关键之处在于常量池包含对该类所用的其它类和方法的所有引用,还包含了 该类及其方法的实际定义。常量池往往占到二进制类大小的一半或更多,但平均下来可能要 少一些。
常量池后面还有几项,它们引用了类本身、其超类以及接口的常量池项。这些项后面是有 关字段和方法的信息,它们本身用复杂结构表示。方法的可执行代码以包含在方法定义中的 代码属性的形式出现。用 JVM 的指令形式表示该代码,一般称为 字节码,这是下一节要讨 论的主题之一。
在 Java 类格式中, 属性被用于几个已定义的用途,包括已提到的字节码、字段的常量 值、异常处理以及调试信息。但是属性并非只可能用于这些用途。从一开始,JVM 规范就已 经要求 JVM 忽略未知类型的属性。这一要求所带来的灵活性使得将来可以扩展属性的用法以 满足其它用途,例如提供使用用户类的框架所需的元信息,这种方法在 Java 派生的 C# 语 言中已广泛使用。遗憾的是,对于在用户级利用这一灵活性还没有提供任何挂钩。