了解了类加载器的来龙去脉,你将可以让你的程序具有强大的动态性----在Java虚拟机不重启的情况下做出具有载入新类的功能;不关闭Java虚拟机的情况下,释放类所占用的记忆体,记忆体不会因为充满了同一个类的多个版本而面临记忆体不足的窘境。
类加载器的功能,就是把类从静态的硬盘里(.class文件),复制一份放到记忆体之中,并做一些初始化的工作,让这个类“活起来”,其他人就能够使用它的功能。类加载器是构成JRE的其中一个重要成员。
自己编写的类只会在用到的时候才载入,称为依需求载入;基础类库是一次性载入的,称为预先载入,这是因为基础类库里头大多是Java程序执行时必备的类,所以为了不要老师做浪费时间的I/O动作(读取文档系统,然后载入记忆体),预先载入这些类库会让Java应用程序在执行时速度稍快一些。依需求载入时,仅仅声明一个类型是不会被加载的,只有实例化才会加载,其有点是节省记忆体,缺点是当第一次用到该类时,系统需要花一些时间来加载该类。(注:如果一个类有父类,载入它之前会先载入父类,类加载器会依继承体系最上层的类往下依序载入)
Java提供两种方法来达成动态行,一种是隐式的,另一种是显式的。这两种方式底层用到的机制完全相同,差异只有程序代码不同。隐式的就是当用到new这个Java关键字时,会让类加载器依需求载入所需的类。显式的又分为两种方法:一种是借用java.lang.Class里的forName()方法,另一种则是借用java.lang.ClassLoader里的loadClass()方法。
使用显式的方法达成动态性,可以在不修改主程序的情况下增加主程序的功能:
public interface Assembly{
public void start();
}
public class Office{
public static void mail(String args[]) throws Exception{
Class c=Class.forName(arg[0]);
Object o=c.newInstance();
Assembly a=(Assembly)o;
A.start();
}
}
public class Word implements Assembly{
public void start(){
System.out.println("Word starts");
}
}
public class Excel implements Assembly{
public void start(){
System.out.println("Excel starts");
}
}
如此以来,我们的主程序Office.java只要编译之后,往后只要调用java Office Word或java Office Excel就可以动态载入我们需要的类。
实际上,JAVA 2SDK中有两个forName()方法:
Public static Class forName(String className)
Public static Class forNmae(String name,boolean initialize,ClassLoader loader)
这两个方法,最后都是连接到原生方法forName0()中,其声明如下:
Private static native Class forName0(String name,boolean initialize,ClassLoader loader) throws ClassNotFoundException;
只有一个参数的forName()方法:
forName(className,true,ClassLoader.getCallerClassLoader());
而具有三个参数的forName()方法最后会调用:
forName(name,initialize,loader);
public class Office{
public static void mail(String args[]) throws Exception{
Office off=new Office();
Class c=Class.forName(arg[0],true,off.getClass().getClassLoader());
Object o=c.newInstance();
Assembly a=(Assembly)o;
A.start();
}
}
驻:第三个参数用来指定载入类的类加载器,。ClassLoader.getCallerClassLoader()是一个private方法,所以我们无法自行调用,因此必需要自己产生一个Office类的实体,再去取得载入Office类时所使用的类加载器。第二个参数为false时,即使类被载入了,其静态初始化代码块也没有被调用,而是在实例化时才真正被调用(过去很多书本都说静态初始化代码块是在类第一次载入识被调用的,这其实是不对的)。
另一种用显式的方法来达成动态性:直接使用类加载器
在Java中,每个类最终的老祖宗都是object,而object里面有一个方法getClass(),就是用来取得某特定类的参考,这个参考,指向的是一个名为Class类(Class.class)的实体,你无法自行产生一个Class类的实体,因为它被声明为Private,这个Class类的实体是在类(.class)第一次载入内存时就建立的,以后你的程序中产生任何该类的实体,这些实体的内部都会有一个栏位记录着这个Class类别的所在位置。如下图:
基本上,我们可以把每个Class类的实体,当作是某个类在内存中的代理人。每次我们需要查询该类的资料(如其中的field,method等)时,就可以请这个实体帮我们代劳。事实上,Java的反射机制,就大量地应用了Class类。
在Java中,每个类都是有某个类加载器(ClassLoader的实体)来载入,因此,Class类的实体中,都会有栏位记录载入它的ClassLoader的实体(注意:如果该栏位是null,并不代表它不是由类加载器所载入,而是代表这个类由靴带式加载器(bootstrap loader,也有人称root loader)所载入,只不过因为这个加载器不是用Java所编写,所以逻辑上没有实体)。如下图:
从上图可知,系统里同时存在多个ClassLoader的实体,而且一个类加载器不仅限于只能载入一个类,类加载器可以载入多个类。所以,只要取得Class类实体的参考,就可以利用其getClassLoader()方法取得载入该类的类加载器的参考。最后,取得了ClassLoader的实体,我们就可以调用其loadClass()方法帮我们载入我们想要的类。因此把程序代码修改如下:
Public class Office{
Public static void main(String args[]) throws Exception{
Office off=new Office();
System.out.println("类准备载入");
ClassLoader loader=off.getClass().getClassLoader();
Class c=loader.loadClass(arg[0]);
System.out.println("类准备实例化");
Object o=c.newInstance();
Object o2=c.newInstance();
}
}
注意:直接使用ClassLoader的loadClass()方法来载入类,只会把类载入内存,并不会调用该类的静态初始化块,而必需等到第一次实例化该类别时才会调用。这种情形与使用Class类的forName()方法时,第二个参数传入false几乎是相同的结果。
上述代码还有另一种写法:
Public class Office{
Public static void main(String args[]) throws Exception{
Class cb=Office.class;
System.out.println("类准备载入");
ClassLoader loader=cb.getClassLoader();
Class c=loader.loadClass(arg[0]);
System.out.println("类准备实例化");
Object o=c.newInstance();
Object o2=c.newInstance();
}
}
直接在程序里使用Office.class,比起产生Office的实体,再用getClass(0取出,这个方法方便的多,也比较节省内存。
自己建立类加载器来载入类别:
在此之前,我们都是使用既有类的载入器来帮我们载入所指定的类。我们还可以利用自己产生的类加载器来载入类,Java本省提供的java.net.URLClassLoader类就可以做到:
Public class Office{
Public static void main(String args[]) throws Exception{
URL u=new URL("file:/d:/my/lib/");//指定搜索类的路径
URLClassLoaer ucl=new RULClassLoader(new URL[]{u});
Class c=ucl.loadClass(arg[0]);
Assembly asm=(Assembly )c.newInstance();
Asm.start();
}
}
类被那个类加载器载入?
将上述程序修改如下:
Public class Office{
Public static void main(String args[]) throws Exception{
URL u=new URL("file:/d:/my/lib/");//指定搜索类的路径
URLClassLoaer ucl=new RULClassLoader(new URL[]{u});
Class c=ucl.loadClass(arg[0]);
Assembly asm=(Assembly )c.newInstance();
Asm.start();
URL u1=new URL("file:/d:/my/lib/");//指定搜索类的路径
URLClassLoaer ucl1=new RULClassLoader(new URL[]{u1});
Class c1=ucl1.loadClass(arg[0]);
Assembly asm1=(Assembly )c1.newInstance();
Asm1.start();
System.out.println(Office.class.getClassLoader());
System.out.println(u.getClass().getClassLoader());
System.out.println(ucl.getClass().getClassLoader());
System.out.println(c.getClassLoader());
System.out.println(asm.getClass().getClassLoader());
System.out.println(u1.getClass().getClassLoader());
System.out.println(ucl1.getClass().getClassLoader());
System.out.println(c1.getClassLoader());
System.out.println(asm1.getClass().getClassLoader());
}
}
执行输出结果如下:
由图可知,Office.class由AppClassLoader(又称SystemLoader,系统加载器)所载入,URL.class与RULClassLoader.class由Bootstrap Loader(非Java编写所以为null)所载入。而Word.class分别由两个不同的RULClassLoader载入。至于Assembly.class,本身应该是由AppClassLoader载入,但是由于多态的关系,所指向的类别实体(Word.class)由特定的加载器加载,导致屏幕上的内容是由其所参考类的实体的加载器,所以在执行getClassLoader()的时候,调用的一定是所参考的类的实体的getClassLoader(),要知道interface本身是由哪个加载器载入,你必需使用如下代码:Assembly.class.getClassLoader()。(注:Assembly.class是有AppClassLoader载入的)
未完待续....