概要本文通过查看一个精心构造的类结构的运行输出和使用javap工具查看实际生成的java字节码(bytecode)向java程序员展示了一个类在运行时是如何构造生成的
关键字 java 构造 javap 字节码 bytecode
按照java规范一个类实例的构造过程是遵循以下顺序的
如果构造方法(constructor也有翻译为构造器和构造函数的)是有参数的则进行参数绑定
内存分配将非静态成员赋予初始值(原始类型的成员的值为规定值例如int型为float型为fboolean型为false对象类型的初始值为null)静态成员是属于类对象而非类实例所以类实例的生成不进行静态成员的构造或者初始化后面将讲述静态成员的生成时间
如果构造方法中存在this()调用(可以是其它带参数的this()调用)则执行之执行完毕后进入第步继续执行如果没有this调用则进行下一步
执行显式的super()调用(可以是其它带参数的super()调用)或者隐式的super()调用(缺省构造方法)此步骤又进入一个父类的构造过程并一直上推至Object对象的构造
执行类申明中的成员赋值和初始化块
执行构造方法中的其它语句
现在来看看精心构造的一个实例
class Parent
{
int pm;
int pm=;
int pm=pmethod();
{
Systemoutprintln(Parents instance initialize block);
}
public static int spm=;
static
{
Systemoutprintln(Parents static initialize block);
}
Parent()
{
Systemoutprintln(Parents default constructor);
}
static void staticmethod()
{
Systemoutprintln(Parents staticmethod);
}
int pmethod()
{
Systemoutprintln(Parents method);
return ;
}
}
class Child extends Parent
{
int cm;
int cm=;
int cm=cmethod();
Other co;
public static int scm=;
{
Systemoutprintln(Childs instance initialize block);
}
static
{
Systemoutprintln(Childs static initialize block);
}
Child()
{
co=new Other();
Systemoutprintln(Childs default constructor);
}
Child(int m)
{
this();
cm=m;
Systemoutprintln(Childs selfdefine constructor);
}
static void staticmethod()
{
Systemoutprintln(Childs staticmethod);
}
int cmethod()
{
Systemoutprintln(Childs method);
return ;
}
}
class Other
{
int om;
Other() {
Systemoutprintln(Others default constructor);
}
}
public class InitializationTest
{
public static void main(String args[])
{
Child c;
Systemoutprintln(program start);
Systemoutprintln(Childscm);
c= new Child();
Systemoutprintln(program end);
}
}
进入此文件所在的目录然后
编译此文件javac InitializationTestjava
运行此程序java ?classpath InitializationTest
得到的结果是
program start
Parents static initialize block
Childs static initialize block
Parents method
Parents instance initialize block
Parents default constructor
Childs method
Childs instance initialize block
Others default constructor
Childs default constructor
Childs selfdefine constructor
program end
如果没有看过上面的关于类的构造的说明很容易让人误解为类的构造顺序是如下的结果(忽略参数绑定内存分配和非静态成员的缺省值赋值)
完成父类的非静态成员初始化赋值以及执行初始化块(这个的先后顺序取决于源文件中的书写顺序可以将初始化块置于成员声明前那么先执行的将是初始化块将上面的代码稍稍变动一下就可以验证这一点)
调用父类的构造方法完成父类构造
完成非静态成员的初始化赋值以及执行初始化块
调用构造方法完成对象的构造执行构造方法体中的其它内容
如果根据以上java规范中给出的顺序也可以合理的解释程序的输出结果那么如何亲眼看到是规范中的顺序而不是以上根据程序的输出推断的顺序呢?
下面就使用JDK自带的javap工具看看实际的顺序这个工具是一个根据编译后的字节码生成一份字节码的助记符格式的文档的工具就像根据机器码生成汇编代码那样
反编译javap c classpath Child
输出的结果是(已经经过标记交替使用黑体和斜体表示要讲解的每一块)
Compiled from InitializationTestjava
class Child extends Parent {
int cm;
int cm;
int cm;
Other co;
public static int scm;
static {};
Child();
Child(int);
int cmethod();
static void staticmethod();
}
Method static {}
bipush
putstatic # getstatic # ldc # invokevirtual # return
Method Child()
aload_
invokespecial # aload_
bipush
putfield # aload_
aload_
invokevirtual # putfield # getstatic # ldc # invokevirtual # aload_
new # dup
invokespecial # putfield # getstatic # ldc # invokevirtual # return
Method Child(int)
aload_
invokespecial # aload_
iload_
putfield # getstatic # ldc # invokevirtual # return
Method int cmethod()
getstatic # ldc # invokevirtual # iconst_
ireturn
Method void staticmethod()
getstatic # ldc # invokevirtual # return
请仔细浏览一下这个输出并和源代码比较一下
下面解释如何根据这个输出得到类实例的实际的构造顺序在开始说明前先解释一下输出的语句的格式语句中最前面的一个数字是指令的偏移值这个我们在此可以不管第二项是指令助记符可以从字面上大致看出指令的意思
例如 getstatic 指令将一个静态成员压入一个称为操作数堆栈(后续的指令就可以引用这个数据结构中的成员)的数据结构而 invokevirtual 指令是调用java虚拟机方法第三项是操作数(#号后面跟一个数字实际上是类的成员的标记)有些指令没有这一项因为有些指令如同汇编指令中的某些指令一样是不需要操作数的(可能是操作数是隐含的或者根本就不需要)这是java中的一个特色
如果你直接检查字节码你会看到成员信息没有直接嵌入指令而是像所有由java类使用的常量那样存储在一个共享池中将成员信息存储在一个常量池中可以减小字节码指令的大小因为指令只需要存储常量池中的一个索引而不是整个常量
需要说明的是常量池中的项目的顺序是和编译器相关的因此在你的环境中看到的可能和我上面给出的输出不完全一样第四项是对前面的操作数的说明实际的字节码中也是没有的根据这个你能很清楚的得到实际上使用的是哪个成员或者调用的是哪个方法这也是javap为我们提供的便利
说完上面这些你现在应该很容易看懂上面的结果和下面将要叙述的