Java虚拟机类装载的原理及实现
一、引言
Java虚 拟机(JVM)的类装载就是指将包含在类文件中的字节码装载到JVM中, 并使其成为JVM一部分的过程。JVM的类动态装载技术能够在运行时刻动态地加载或者替换系统的某些功能模块, 而不影响系统其他功能模块的正常运行。本文将分析JVM中的类装载系统,探讨JVM中类装载的原理、实现以及应用。
二、Java虚拟机的类装载实现与应用
2.1 装载过程简介
所谓装载就是寻找一个类或是一个接口的二进制形式并用该二进制形式来构造代表这个类或是这个接口的class对象的过程,其中类或接口的名称是给定了的。当然名称也可以通过计算得到,但是更常见的是通过搜索源代码经过编译器编译后所得到的二进制形式来构造。
在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工作如下:
装载:查找和导入类或接口的二进制数据;
链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;
校验:检查导入类或接口的二进制数据的正确性;
准备:给类的静态变量分配并初始化存储空间;
解析:将符号引用转成直接引用;
初始化:激活类的静态变量的初始化Java代码和静态Java代码块。
至于在类装载和虚拟机启动的过程中的具体细节和可能会抛出的错误,请参看《 Java虚拟机规范》以及《 深入Java虚拟机》。 由于本文的讨论重点不在此就不再多叙述。
2.2 装载的实现
JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader 是一个重要的Java运行时系统组件。它负责在运行时查找和装入类文件的类。
在Java中,ClassLoader是一个抽象类,它在包java.lang中,可以这样说,只要了解了在ClassLoader中的一些重要的方 法,再结合上面所介绍的JVM中类装载的具体的过程,对动态装载类这项技术就有了一个比较大概的掌握,这些重要的方法包括以下几个:
①loadCass方法 loadClass(String name ,boolean resolve)其中name参数指定了JVM需要的类的名称,该名称以包表示法表示,如Java.lang.Object;resolve参数告诉方法 是否需要解析类,在初始化类之前,应考虑类解析,并不是所有的类都需要解析,如果JVM只需要知道该类是否存在或找出该类的超类,那么就不需要解析。这个 方法是ClassLoader 的入口点。
②defineClass方法 这个方法接受类文件的字节数组并把它转换成Class对象。字节数组可以是从本地文件系统或 网络装入的数据。它把字节码分析成运行时数据结构、校验有效性等等。
③findSystemClass方法 findSystemClass方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass将字节数组转换成Class对象,以将该文件转换成类。当运行Java应用 程序时,这是JVM 正常装入类的缺省机制。
④resolveClass方法 resolveClass(Class c)方法解析装入的类,如果该类已经被解析过那么将不做处理。当调用loadClass方法时,通过它的resolve 参数决定是否要进行解析。
⑤findLoadedClass方法 当调用loadClass方法装入类时,调用findLoadedClass 方法来查看ClassLoader是否已装入这个类,如果已装入,那么返回Class对象,否则返回NULL。如果强行装载已存在的类,将会抛出链接错误。
2.3 装载的应用
一般来说,我们使用虚拟机的类装载时需要继承抽象类 java.lang.ClassLoader, 其中必须实现的方法是loadClass(),对于这个方法需要实现如下操作:(1) 确认类的名称;(2) 检查请求要装载的类是否已经被装载;(3) 检查请求加载的类是否是系统类;(4) 尝试从类装载器的存储区获取所请求的类;(5) 在虚拟机中定义所请求的类;(6) 解析所请求的类;(7) 返回所请求的类。
所有的Java 虚拟机都包括一个内置的类装载器,这个内置的类库装载器被称为根装载器(bootstrap ClassLoader)。根装载器的特殊之处是它只能够装载在 设计时刻已知的类,因此虚拟机假定由根装载器所装载的类都是 安全的、可信任的,可以不经过安全认证而直接运行。当应用 程序需要加载并不是设计时就知道的类时,必须使用用户自定义的装载器(user-defined ClassLoader)。下面我们举例说明它的应用。
public abstract class MultiClassLoader extends ClassLoader{ ... public synchronized Class loadClass(String s, boolean flag) throws ClassNotFoundException { /* 检查类s是否已经在本地内存*/ Class class1 = (Class)classes.get(s); /* 类s已经在本地内存*/ if(class1 != null) return class1; try/*用默认的ClassLoader 装入类*/ { class1 = super.findSystemClass(s); return class1; } catch(ClassNotFoundException _ex) { System.out.println(">> Not a system class."); } /* 取得类s的字节数组*/ byte abyte0[] = loadClassBytes(s); if(abyte0 == null) throw new ClassNotFoundException(); /* 将类字节数组转换为类*/ class1 = defineClass(null, abyte0, 0, abyte0.length); if(class1 == null) throw new ClassFormatError(); if(flag) resolveClass(class1); /*解析类*/ /* 将新加载的类放入本地内存*/ classes.put(s, class1); System.out.println(">> Returning newly loaded class."); /* 返回已装载、解析的类*/ return class1; } ... } |
三、Java虚拟机的类装载原理
前面我们已经知道,一个Java应用 程序使用两种类型的类装载器:根装载器(bootstrap)和用户定义的装载器(user-defined)。根装载器是Java虚拟机实现的一部分,举个例子来说,如果一个Java虚拟机是在现在已经存在并且正在被使用的 操作系统的 顶部用C程序来实现的,那么根装载器将是那些C程序的一部分。根装载器以某种默认的方式将类装入,包括那些Java API的类。在运行期间一个Java程序能安装用户自己定义的类装载器。根装载器是虚拟机固有的一部分,而用户定义的类装载器则不是,它是用Java语言 写的,被编译成class文件之后然后再被装入到虚拟机,并像其它的任何对象一样可以被实例化。 Java类装载器的体系结构如下所示:
![]() 图1 Java的类装载的体系结构 |
Java的类装载 模型是 一种代理(delegation)模型。当JVM 要求类装载器CL(ClassLoader)装载一个类时,CL首先将这个类装载请求转发给他的父装载器。只有当父装载器没有装载并无法装载这个类 时,CL才获得装载这个类的机会。这样, 所有类装载器的代理关系构成了一种树状的关系。树的根是类的根装载器(bootstrap ClassLoader) , 在JVM 中它以"null"表示。除根装载器以外的类装载器有且仅有一个父装载器。在创建一个装载器时, 如果没有显式地给出父装载器, 那么JVM将默认系统装载器为其父装载器。Java的基本类装载器代理结构如图2所示:
![]() 图2 Java类装载的代理结构 |
下面针对各种类装载器分别进行详细的说明。
根(Bootstrap) 装载器:该装载器没有父装载器,它是JVM实现的一部分,从sun.boot.class.path装载运行时库的核心代码。
扩展(Extension) 装载器:继承的父装载器为根装载器,不像根装载器可能与运行时的操作系统有关,这个类装载器是用纯Java代码实现的,它从java.ext.dirs (扩展目录)中装载代码。
系统(System or Application) 装载器:装载器为扩展装载器,我们都知道在安装JDK的时候要设置环境变量(CLASSPATH ),这个类装载器就是从java.class.path(CLASSPATH 环境变量)中装载代码的,它也是用纯Java代码实现的,同时还是用户自定义类装载器的缺省父装载器。
小应用程序(Applet) 装载器: 装载器为系统装载器,它从用户指定的 网络上的特定目录装载小应用程序代码。
在 设计一个类装载器的时候,应该满足以下两个条件:
对于相同的类名,类装载器所返回的对象应该是同一个类对象
如果类装载器CL1将装载类C的请求转给类装载器CL2,那么对于以下的类或接口,CL1和CL2应该返回同一个类对象:a)S为C的直接超类;b)S 为C的直接超接口;c)S为C的成员变量的类型;d)S为C的成员方法或构建器的参数类型;e)S为C的成员方法的返回类型。
每 个已经装载到JVM中的类都隐式含有装载它的类装载器的信息。类方法getClassLoader 可以得到装载这个类的类装载器。一个类装载器认识的类包括它的父装载器认识的类和它自己装载的类,可见类装载器认识的类是它自己装载的类的超集。注意我们 可以得到类装载器的有关的信息,但是已经装载到JVM中的类是不能更改它的类装载器的。
Java中的类的装载过程也就是代理装载 的过程。比如:Web浏览器中的JVM需要装载一个小应用程序TestApplet。JVM调用小应用程序装载器ACL(Applet ClassLoader)来完成装载。ACL首先请求它的父装载器, 即系统装载器装载TestApplet是否装载了这个类, 由于TestApplet不在系统装载器的装载路径中, 所以系统装载器没有找到这个类, 也就没有装载成功。接着ACL自己装载TestApplet。ACL通过网络成功地找到了TestApplet.class 文件并将它导入到了JVM中。在装载过程中, JVM发现TestAppet是从超类java.applet.Applet继承的。所以JVM再次调用ACL来装载 java.applet.Applet类。ACL又再次按上面的顺序装载Applet类, 结果ACL发现他的父装载器已经装载了这个类, 所以ACL就直接将这个已经装载的类返回给了JVM , 完成了Applet类的装载。接下来,Applet类的超类也一样处理。最后, TestApplet及所有有关的类都装载到了JVM中。
四、结论
类的动态 装载机制是JVM的一项核心技术, 也是容易被忽视而引起很多误解的地方。本文介绍了JVM中类装载的原理、实现以及应用,尤其分析了ClassLoader的结构、用途以及如何利用自定义 的ClassLoader装载并执行Java类,希望能使读者对JVM中的类装载有一个比较深入的理解。
Java 虚拟机类装载体系
装载:
把二进制形式的java类型读入Java虚拟机中
通过该类型的完全限定名,产生一个代表该类型的二进制数据流。
解析这个二进制数据流为方法区的内部数据结构。
在堆上创建一个表示该类型的java.lang.Class类的实例。
1、何时装载
(1)隐式装载
package test;
Public class A{
public void static main(String args[]){
B b = new B();
}
}
class B{C c;}
class C{}
A、B、C类装载顺序:A 、B (C不装载)
(2)显示装载
A、使用Class类的forName方法。它可以指定装载器,也可以使用装载当前类的装载器。例如:
Class.forName("test.A");
B、使用类路径类装载装载.
ClassLoader.getSystemClassLoader().loadClass("test.A");
C、使用当前进程上下文的使用的类装载器进行装载,这种装载类的方法常常被有着复杂类装载体系结构的系统所使用。
Thread.currentThread().getContextClassLoader().loadClass(“test.A”) D、使用自定义的类装载器装载类
public class MyClassLoader extends URLClassLoader{ public MyClassLoader() { super(new URL[0]); }
}
MyClassLoader myClassLoader = new MyClassLoader(); myClassLoader.loadClass("test.A");
2、谁来装载——java虚拟机的两种类装载器
A、系统类装载器——Bootstrap(API)
Java虚拟机实现的一部分,有可能是C++编写。
B、自定义类装载器
普通的Java对象,必须派生自 java.lang.ClassLoder
例子:
标准扩展类装载器:ExtClassLoader(javax、lib/ext)
系统(类路径)类装载器:AppClassLoader(classPath)
任意继承自java.lang.ClassLoder的类
3、双亲委派模型(不是类继承关系)
在双亲委派模式下,双亲可以抢在子孙的前面加载类,因此安全。
4、类装载器和命名空间
默认条件下某个类只能看见用同一个类装载器装载(同一命名空间)的其它类。
用不同的类载入程序装入的类在不同的命名空间中,并且除非明确许可外都不能互相访问。
5、类装载体系和Java安全模式
把代码分离到不同的命名空间并在不同命名空间的代码间设置保护屏;
保护象JavaAPI这样已获确认的库。
Java类装载体系中的隔离性
1. Java类装载体系结构
装载类的过程非常简单:查找类所在位置,并将找到的Java类的字节码装入内存,生成对应的Class对象。 Java的类装载器专门用来实现这样的过程,JVM并不止有一个类装载器,事实上,如果你愿意的话,你可以让JVM拥有无数个类装载器,当然这除了测试 JVM外,我想不出还有其他的用途。你应该已经发现到了这样一个问题,类装载器自身也是一个类,它也需要被装载到内存中来,那么这些类装载器由谁来装载 呢,总得有个根吧?没错,确实存在这样的根,它就是神龙见首不见尾的Bootstrap ClassLoader. 为什么说它神龙见首不见尾呢,因为你根本无法在Java代码中抓住哪怕是它的一点点的尾巴,尽管你能时时刻刻体会到它的存在,因为java的运行环境所需 要的所有类库,都由它来装载,而它本身是C++写的程序,可以独立运行,可以说是JVM的运行起点,伟大吧。在Bootstrap完成它的任务后,会生成 一个AppClassLoader(实际上之前系统还会使用扩展类装载器ExtClassLoader,它用于装载Java运行环境扩展包中的类),这个 类装载器才是我们经常使用的,可以调用ClassLoader.getSystemClassLoader() 来获得,我们假定程序中没有使用类装载器相关操作设定或者自定义新的类装载器,那么我们编写的所有java类通通会由它来装载,值得尊敬吧。 AppClassLoader查找类的区域就是耳熟能详的Classpath,也是初学者必须跨过的门槛,有没有灵光一闪的感觉,我们按照它的类查找范围 给它取名为类路径类装载器。还是先前假定的情况,当Java中出现新的类,AppClassLoader首先在类传递给它的父类类装载器,也就是 Extion ClassLoader,询问它是否能够装载该类,如果能,那AppClassLoader就不干这活了,同样Extion ClassLoader在装载时,也会先问问它的父类装载器。我们可以看出类装载器实际上是一个树状的结构图,每个类装载器有自己的父亲,类装载器在装载 类时,总是先让自己的父类装载器装载(多么尊敬长辈),如果父类装载器无法装载该类时,自己就会动手装载,如果它也装载不了,那么对不起,它会大喊一 声:Exception,class not found。有必要提一句,当由直接使用类路径装载器装载类失败抛出的是NoClassDefFoundException异常。如果使用自定义的类装载 器loadClass方法或者ClassLoader的findSystemClass方法装载类,如果你不去刻意改变,那么抛出的是 ClassNotFoundException。
我们简短总结一下上面的讨论:
1.JVM类装载器的体系结构可以看作是树状结构。
2.父类装载器优先装载。在父类装载器装载失败的情况下再装载,如果都装载失败则抛出ClassNotFoundException或者NoClassDefFoundError异常。
那么我们的类在什么情况下被装载的呢?
2. 类如何被装载在java2中,JVM是如何装载类的呢,可以分为两种类型,一种是隐式的类装载,一种式显式的类装载。
2.1 隐式的类装载隐式的类装载是编码中最常用得方式:
A b = new A();
如果程序运行到这段代码时还没有A类,那么JVM会请求装载当前类的类装器来装载类。 问题来了,我把代码弄得复杂一点点,但依旧没有任何难度,请思考JVM得装载次序:
package test;
Public class A{
public void static main(String args[]){
B b = new B();
}
}
class B{C c;}
class C{}
揭晓答案,类装载的次序为A->B,而类C根本不会被JVM理会,先不要惊讶,仔细想想,这不正是我们最需要 得到的结果。我们仔细了解一下JVM装载顺序。当使用Java A命令运行A类时,JVM会首先要求类路径类装载器(AppClassLoader)装载A类,但是这时只装载A,不会装载A中出现的其他类(B类),接 着它会调用A中的main函数,直到运行语句b = new B()时,JVM发现必须装载B类程序才能继续运行,于是类路径类装载器会去装载B类,虽然我们可以看到B中有有C类的声明,但是并不是实际的执行语句, 所以并不去装载C类,也就是说JVM按照运行时的有效执行语句,来决定是否需要装载新类,从而装载尽可能少的类,这一点和编译类是不相同的。
2.2 显式的类装载
使用显示的类装载方法很多,我们都装载类test.A为例。
使用Class类的forName方法。它可以指定装载器,也可以使用装载当前类的装载器。例如:
Class.forName("test.A");
它的效果和
Class.forName("test.A",true,this.getClass().getClassLoader());
是一样的。
使用类路径类装载装载.
ClassLoader.getSystemClassLoader().loadClass("test.A");
使用当前进程上下文的使用的类装载器进行装载,这种装载类的方法常常被有着复杂类装载体系结构的系统所使用。
Thread.currentThread().getContextClassLoader().loadClass("test.A")
使用自定义的类装载器装载类
public class MyClassLoader extends URLClassLoader{
public MyClassLoader() {
super(new URL[0]);
}
}
MyClassLoader myClassLoader = new MyClassLoader();
myClassLoader.loadClass("test.A");
MyClassLoader继承了URLClassLoader类,这是JDK核心包中的类装载器,在没有指定父类装载器的情况下,类路径类装载器就是它的父类装载器,MyClassLoader并没有增加类的查找范围,因此它和类路径装载器有相同的效果。
我们已经知道Java的类装载器体系结构为树状,多个类装载器可以指定同一个类装载器作为自己的父类,每个子类装载 器就是树状结构的一个分支,当然它们又可以个有子类装载器类装载器,类装载器也可以没有父类装载器,这时Bootstrap类装载器将作为它的隐含父类, 实际上Bootstrap类装载器是所有类装载器的祖先,也是树状结构的根。这种树状体系结构,以及父类装载器优先的机制,为我们编写自定义的类装载器提 供了便利,同时可以让程序按照我们希望的方式进行类的装载。例如某个程序的类装载器体系结构图如下:
图2:某个程序的类装载器的结构
解释一下上面的图,ClassLoaderA为自定义的类装载器,它的父类装载器为类路径装载器,它有两个子类装载 器ClassLoaderAA和ClassLaderAB,ClassLoaderB为程序使用的另外一个类装载器,它没有父类装载器,但有一个子类装载 器ClassLoaderBB。你可能会说,见鬼,我的程序怎么会使用这么复杂的类装载器结构。为了进行下面的讨论,暂且委屈一下。
3. 奇怪的隔离性
我们不难发现,图2中的类装载器AA和AB, AB和BB,AA和B等等位于不同分支下,他们之间没有父子关系,我不知道如何定义这种关系,姑且称他们位于不同分支下。两个位于不同分支的类装载器具有 隔离性,这种隔离性使得在分别使用它们装载同一个类,也会在内存中出现两个Class类的实例。因为被具有隔离性的类装载器装载的类不会共享内存空间,使 得使用一个类装载器不可能完成的任务变得可以轻而易举,例如类的静态变量可能同时拥有多个值(虽然好像作用不大),因为就算是被装载类的同一静态变量,它 们也将被保存不同的内存空间,又例如程序需要使用某些包,但又不希望被程序另外一些包所使用,很简单,编写自定义的类装载器。类装载器的这种隔离性在许多 大型的软件应用和服务程序得到了很好的应用。下面是同一个类静态变量为不同值的例子。
package test;
public class A {
public static void main( String[] args ) {
try {
//定义两个类装载器
MyClassLoader aa= new MyClassLoader();
MyClassLoader bb = new MyClassLoader();
//用类装载器aa装载testb.B类
Class clazz=aa.loadClass("testb. B");
Constructor constructor=
clazz.getConstructor(new Class[]{Integer.class});
Object object =
constructor.newInstance(new Object[]{new Integer(1)});
Method method =
clazz.getDeclaredMethod("printB",new Class[0]);
//用类装载器bb装载testb.B类
Class clazz2=bb.loadClass("testb. B");
Constructor constructor2 =
clazz2.getConstructor(new Class[]{Integer.class});
Object object2 =
constructor2.newInstance(new Object[]{new Integer(2)});
Method method2 =
clazz2.getDeclaredMethod("printB",new Class[0]);
//显示test.B中的静态变量的值
method.invoke( object,new Object[0]);
method2.invoke( object2,new Object[0]);
} catch ( Exception e ) {
e.printStackTrace();
}
}
}
//Class B 必须位于MyClassLoader的查找范围内,
//而不应该在MyClassLoader的父类装载器的查找范围内。
package testb;
public class B {
static int b ;
public B(Integer testb) {
b = testb.intValue();
}
public void printB() {
System.out.print("my static field b is ", b);
}
}
public class MyClassLoader extends URLClassLoader{
private static File file = new File("c://classes ");
//该路径存放着class B,但是没有class A
public MyClassLoader() {
super(getUrl());
}
public static URL[] getUrl() {
try {
return new URL[]{file.toURL()};
} catch ( MalformedURLException e ) {
return new URL[0];
}
}
}
程序的运行结果为:
my static field b is 1
my static field b is 2
程序的结果非常有意思,从编程者的角度,我们甚至可以把不在同一个分支的类装载器看作不同的java虚拟机,因为它 们彼此觉察不到对方的存在。程序在使用具有分支的类装载的体系结构时要非常小心,弄清楚每个类装载器的类查找范围,尽量避免父类装载器和子类装载器的类查 找范围中有相同类名的类(包括包名和类名),下面这个例子就是用来说明这种情况可能带来的问题。
假设有相同名字却不同版本的接口 A,
版本 1:
package test;
Intefer Same{ public String getVersion(); }
版本 2:
Package test;
Intefer Same{ public String getName(); }
接口A两个版本的实现:
版本1的实现
package test;
public class Same1Impl implements Same {
public String getVersion(){ return "A version 1";}
}
版本2的实现
public class Same 2Impl implements Same {
public String getName(){ return "A version 2";}
}
我们依然使用图2的类装载器结构,首先将版本1的Same和Same的实现类Same1Impl打成包 same1.jar,将版本2的Same和Same的实现类Same1Impl打成包same2.jar。现在,做这样的事情,把same1.jar放入 类装载器ClassLoaderA的类查找范围中,把same2.jar放入类装器ClassLoaderAB的类查找范围中。当你兴冲冲的运行下面这个 看似正确的程序。
实际上这个错误的是由父类载器优先装载的机制造成,当类装载器ClassLoaderAB在装载Same2Impl 类时发现必须装载接口test.Same,于是按规定请求父类装载器装载,父类装载器发现了版本1的test.Same接口并兴冲冲的装载,但是却想不到 Same2Impl所希望的是版本2 的test.Same,后面的事情可想而知了,异常被抛出。
我们很难责怪Java中暂时并没有提供区分版本的机制,如果使用了比较复杂的类装载器体系结构,在出现了某个包或者类的多个版本时,应特别注意。
掌握和灵活运用Java的类装载器的体系结构,对程序的系统设计,程序的实现,已经程序的调试,都有相当大的帮助。类初始化和装载顺序
类和继承从语法角度上说,没有什么难点在。类,是java的基本单位,也是OOP语言的体现。从C++开始,我看的每本书的前面一两章都是十分繁琐 的介绍面向对象的知识,老实说我从来没有仔细去看过,但是我记住了一句话,万事万物皆对象。OK,我觉得对类的理解就这样好了。从而,我把类看作一个箱 子,然后把一些认为是在一类的东西统统放里面去,就编码来说,把你自己认为是同一类(或者在程序中实现一个功能的模块)的代码统统给放到这个箱子里面去, 然后域(变量)尽量是private的,通过public的方法(函数)去操作域的读写,这样就可以了。关于类就简单说一下,下面介绍一下类的初始化过 程。
1、类只有在使用New调用创建的时候才会被JAVA类装载器装入
2、JAVA类首次装入时,会对静态成员变量或方法进行一次初始化,但方法不被调用是不会执行的,静态成员变量和静态初始化块级别相同,非静态成员变量和非静态初始化块级别相同。
先初始化父类的静态代码--->初始化子类的静态代码-->
初始化父类的非静态代码--->初始化父类构造函数--->
初始化子类非静态代码--->初始化子类构造函数
3、创建类实例时,首先按照父子继承关系进行初始化
4、类实例创建时候,首先初始化块部分先执行,然后是构造方法;然后从
本类继承的子类的初始化块执行,最后是子类的构造方法
5、类消除时候,首先消除子类部分,再消除父类部分
如果一个类没有父类的话,那么装载的顺序应该是先static(静态初始化),因为静态初始化在一个类第一次new一个对象的时候,首先分配内存, 然后马上初始化静态的东西,我们应该知道,static在同一个类的所有对象里面只有一个拷贝(不知道这样说合不合适,^_^,我想说明的 是,static在内存中只有一个地方来存储,而不是每个对象里面都有不同的内存空间来存在,也就是说,所有的对象使用的是同一个static域,必然, 我们需要先对static进行初始化,因为非static域在new各个对象之后,使用的是不同的内存空间各自来分配的),接下来是类的(非静态)初始化 语句,最后才是构造函数。后面两个如果不理解,你只要记住就可以了,因为java编译器就是这么做的。我简单说明一下,因为构造函数有时候要调用到类本身 的方法(成员函数),而方法可能直接用到这些非静态域的初始化值,那么如果构造函数在初始化语句之前,必然会导致运算结果的不正确。下面给出一个例子:
class Insect {
//1) int i = 9;
int j;
//2) Insect() {
prt("i = " + i + ", j = " + j); //类Insect的构造函数Insect()就用到了i ,j;这里i输出来的结果应该是9 ;
j = 39;
}
//3) static int x1 = prt("static Insect.x1 initialized");
static int prt(String s) {
System.out.println(s);
return 47;
}
}
//Beetle是Insect的子类
public class Beetle extends Insect {
//4) int k = prt("Beetle.k initialized");
//5) Beetle() {
prt("k = " + k);
prt("j = " + j);
}
//6) static int x2 = prt("static Beetle.x2 initialized");
static int prt(String s) {
System.out.println(s);
return 63;
}
public static void main(String[] args) {
prt("Beetle constructor");
Beetle b = new Beetle();
}
}
上面这个例子还包括了一个子类一个父类,相对初始化装载的时候稍微有点麻烦一点,但是总体原则是一样的。根据我的编号,装载的顺序是:3、6、1、 2、4、5。当在装载Beetle类的时候,先去找Beetle.class把他装载进来,在Beetle.class装载到一半的时候,发现他还有个父 类Insect.class,那么需要把Insect.class也装载进来,并且先对他进行初始化,那么(3)是最先被初始化的,接下来父类装载完毕之 后,子类接着也装载进来,这样,(6)接着被初始化。new Beetle()的时候,先分配内存,接着父类的非静态初始化,然后父类构造函数,所以(1)(2)陆续初始化,接着是子类(4)(5)。基本上初始化的 顺序是先父类后子类,先静态,后非静态,最后才是构造函数。