Java编程的动态性,第1部分: 类和类装入

本文是这个新系列文章的第一篇,该系列文章将讨论我称之为 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# 语 言中已广泛使用。遗憾的是,对于在用户级利用这一灵活性还没有提供任何挂钩。

时间: 2024-09-17 04:24:38

Java编程的动态性,第1部分: 类和类装入的相关文章

Java编程的动态性,第5部分: 动态转换类

在经过一段时间的休息之后,Dennis Sosnoski 又回来推出了他的 Java 编程的动态性系 列的第 5 部分.您已在前面的文章中看到了如何编写用于转换 Java 类文件以改变代码行为 的程序.在本期中,Dennis将展示如何使用 Javassist 框架,把转换与实际的类加载过程结 合起来,用以进行灵活的"即时"面向方面的特性处理.这种方法允许您决定想要在运行时 改变的内容,并潜地在每次运行程序时做出不同的修改.在整个过程中,您还将更深入地了 解向JVM 中加载类的一般问题.

Java编程的动态性, 第4部分: 用Javassist进行类转换

厌倦了只能按编写好源代码的方式执行的 Java 类了吗?那么打起精神吧,因为您就要发 现如何将编译器编译好的类进行改造的方法了!在本文中,Java 顾问 Dennis Sosnoski 通 过介绍字节码操作库 Javassist 将他的 Java 编程的动态性系列带入高潮,Javassist 是广 泛使用的 JBoss 应用服务器中加入的面向方面的编程功能的基础.您会看到到用 Javassist 转换现有类的基本内容,并且了解到这种用框架源代码处理类的方法的威力和局限性. 讲过了 Java 类格

Java编程的动态性,第8部分: 用代码生成取代反射

从本系列前面的文章中,您了解到反射的性能比直接访问要慢许多倍,并了解了用 Javassist 和 Apache Byte Code Engineering Library (BCEL)进行classworking.Java 顾问 Dennis Sosnoski 通过演示如何使用运行时 classworking,来用全速前进的生成代码 取代反射代码,从而结束他的 Java 编程的动态性系列. 既然您已经看到了如何使用 Javassist 和 BCEL 框架来进行 classworking ,我将展

Java编程的动态性,第2部分: 引入反射

在" Java编程的动态性,第1部分,"我为您介绍了Java编程类和类装入.该篇文章介绍 了一些Java二进制类格式的相关信息.这个月我将阐述使用Java反射API来在运行时接入和使 用一些相同信息的基础.为了使已经熟知反射基础的开发人员关注本文,我将在文章中包括 反射性能如何与直接接入相比较. 使用反射不同于常规的Java编程,其中它与 元数据--描述其它数据的数据协作.Java语 言反射接入的特殊类型的原数据是JVM中类和对象的描述.反射使您能够运行时接入广泛的类 信息.它甚至使您

Java编程那些事儿86——文件操作之File类使用

11.3 I/O类使用 由于在IO操作中,需要使用的数据源有很多,作为一个IO技术的初学者,从读写文件开始学习IO技术是一个比较好的选择.因为文件是一种常见的数据源,而且读写文件也是程序员进行IO编程的一个基本能力.本章IO类的使用就从读写文件开始. 11.3.1 文件操作 文件(File)是最常见的数据源之一,在程序中经常需要将数据存储到文件中,例如图片文件.声音文件等数据文件,也经常需要根据需要从指定的文件中进行数据的读取.当然,在实际使用时,文件都包含一个的格式,这个格式需要程序员根据需要

Java编程的动态性,第6部分: 利用Javassist进行面向方面的更改

Java 顾问 Dennis Sosnoski 在他的关于 Javassist 框架的三期文章中将精华部分留在 了最后.这次他展现了 Javassist 对搜索-替换的支持是如何使对 Java 字节码的编辑变得 像文本编辑器的"替换所有(Replace All )"命令一样容易的.想报告所有写入特定字段 的内容或者对方法调用中参数的更改中的补丁吗?Javassist 使这变得很容易,Dennis 向您 展示了其做法. 本系列的 第 4 部分和 第 5 部分讨论了如何用 Javassis

Java编程的动态性,第7部分: 用BCEL设计字节码

Apache Byte Code Engineering Library (BCEL)可以深入 Java 类的字节码.可以用它 转换现有的类表示或者构建新的类,因为 BCEL 在单独的 JVM 指令级别上进行操作,所以可 以让您对代码有最强大的控制.不过,这种能力的代价是复杂性.在本文中,Java 顾问 Dennis Sosnoski 介绍了 BCEL 的基本内容,并引导读者完成一个示例 BCEL 应用程序,这 样您就可以自己决定是否值得以这种复杂性来换取这种能力. 在本系列的最后三篇文章中,我

Java编程的动态性,第3部分: 应用反射

命令行参数处理是一项令人厌烦的零碎工作,不管您过去已经处理过多少次了,它好像总 能重新摆在您的面前.与其一遍又一遍地编写同一块代码的不同变种,为什么不利用反射来 简化参数处理的工作呢?Java 顾问 Dennis Sosnoski 向您展示了如何做到这一点.在本文 中,Dennis 简明扼要地介绍了一个开源库,这个库可以使得命令行参数实际上自己处理自己 . 在 上个月的文章中,我介绍了Java Reflection API,并简要地讲述了它的一些基本功能 .我还仔细研究了反射的性能,并且在文章的

Java编程思想里的泛型实现一个堆栈类 分享

觉得作者写得太好了,不得不收藏一下. 对这个例子的理解: //类型参数不能用基本类型,T和U其实是同一类型. //每次放新数据都成为新的top,把原来的top往下压一级,通过指针建立链接. //末端哨兵既是默认构造器创建出的符合end()返回true的节点. 复制代码 代码如下: //: generics/LinkedStack.java // A stack implemented with an internal linked structure. package generics; pub