学习Java语言的朋友都知道,Java中有许多类,而且他们的来源有很多,可以是网络上的,可以是jdk自带的,也可以是我们自己下载的.jar包(数据库的驱动jar(mysql-connector-java-5.1.28-bin.jar)或者是分析、编辑和创建字节码的.jar包(javassist-3.15.0-GA.jar))等,都可是说是通过类加载机制从外部文件中直接将已经编译好的Java字节码文件加载到我们的Java程序中,也就是说,我们未经创建便得到了一个功能强大的类。下面,让我们看看类类加载器的层次结构吧:
1.引导加载器:该加载器可以说是所有类加载器的父类(注意:这个说法严格上来说是不对的),该类加载器是由C++语言编写的,所以该加载器没有父类,用来加载Java语言的核心库(rt.jar),那么什么是Java核心库呢?(我们经常用的String类,Integer类...等等),再次不必深究。
2.扩展类加载器:扩展类加载器继承自引导类加载器,扩展类加载器的主要功能加载(ext.jar)类库,该类库使用java语言编写的。
3.应用程序类加载库:该加载器继承自扩展类加载器,主要功能是加载我门日常在编译器(idea,eclipse等)里面编写的类,这些类的加载,都是由该类加载器完成的。
4.自定义加载器:开发者可根据自己的不同需求继承ClassLoader类实现其自定义的类加载器,实现特定的类加载方式。
要深入了解类加载机制我们就必须了解类加载的机制,接下来我来介绍类加载中的一个重要机制,“双亲代理(委托)机制”!
1.双亲代理机制是什么?:双亲代理机制,是代理机模式中的一种,我们在加载一个类的时候,首先得到该类信息的是最底层的类加载器,“应用程序类加载器”,该加载器得到类“名称”(我在这里的理解就是类名)后不会立即加载,而是上抛给其parent,就这样一层层的上抛,当上抛至“引导类加载器”时如果该加载器中有和其一样的类名,则该类直接加载“自己”的该类到堆区,如果“引导加载器”中不存在与该类相同类类名,则一层层向下抛,如果在那一层发现与其相同的类名,则直接加载“该类加载器中的这个类”到堆区,如果直至应用程序类加载器还没有发现与该类同名的类,则抛出异常(注意:我们自己定义的类,在应用程序类加载器中都能找到相同的类名);
2.双亲代理机制的作用是什么?:试想一下,你自己定义了一个java.lang.String类,那么你让Java核心库中了(java.lang.String)怎么办?你会把核心库中的String类覆盖掉吗?这当然时不现实的。Java核心库中的类是不可被覆盖的,也就是说,双亲代理机制,保护了Java中自带的类(不仅仅是Java核心库中的类)。也就是说,你可定义一个java.lang.String类,但是你永远都用不了该类,因为该类存在于Java核心库中(rt.jar)而引导加载器负责加载该类,所以你不论怎么调用,它到加载的时Java核心库中的String类。
接下来我们来聊一聊自定义类加载器:
1.自定义类加载器不必实现双亲委派机制(当然也可以实现);
2.自定义类加载器灵活性很高,随编程者的意愿,来加载类。
3.自定义类加载器应当继承自ClassLoader类,并重写其findClass()方法。
下面是我手写的一个文件类加载器:
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
public class MyClassLoader extends ClassLoader{
private String rootDir;
public MyClassLoader(String dir) {
this.rootDir=dir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c=null;
//查看此类是否已经加载完毕
c=findLoadedClass(name);
//如加载完毕,直接返回
if(c!=null) {
return c;
}else {//没有加载,使用代理模式,调用其父类类加载器进行双亲委派式加载
try {
c=this.getParent().loadClass(name);
}catch(Exception e) {
e.printStackTrace();
}
//如果父类加载器中存在该类,则由父类加载器加载
if(c!=null) {
return c;
}else {//如果父类类加载器无法加载该类则用自己的类加载器加载
String path;
//构造加载器需要的绝对路径
path=rootDir+"/"+name;
byte[] data=null;
try {
data = getdataArray(path);
} catch (IOException e) {
// e.printStackTrace();//因为父类中没有发现该类相关信息所以报错,当然我们可以删除器双亲代理机制,获知捕获异常来解决这一问题
}
//自定义类
c=defineClass(null,data, 0, data.length);
//如果返回值为空,则说明路径不存在,抛出异常
if(c==null) {
throw new ClassNotFoundException();
}else {//否则返回使用自定义类加载器加载完成的类
return c;
}
}
}
}
//获得绝对路径的byte数组
private byte[] getdataArray(String str) throws IOException {
int len=0;
byte[] car=new byte[1000];
FileInputStream fi=new FileInputStream(str);
ByteArrayOutputStream baos=new ByteArrayOutputStream();
while((len=(fi.read(car)))!=-1) {
baos.write(car, 0, len);
}
return baos.toByteArray();
}
}
下面我们来调用该类加载器:
public class Demo01 {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
MyClassLoader classloader=new MyClassLoader("D:/Temp");//得到第一个文件类加载器对象
MyClassLoader classloader2=new MyClassLoader("D:/Temp");//得到第二个文件类加载器对象
System.out.println(classloader.hashCode());//输出第一个对象的hashcode
System.out.println(classloader2.hashCode());//输出第二个对象的hashcode
System.out.println("******************************");
Class<?> clazz=classloader.loadClass("Demo.class");//使用类加载器一加载类文件
Class<?> clazz2=classloader2.loadClass("Demo.class");//使用类加载器二加载文件
System.out.println(clazz);//输出类
System.out.println(clazz.hashCode());//输出该类的哈希码
System.out.println("***********************");
System.out.println(clazz2);//输该类
System.out.println(clazz2.hashCode());//输出该类的哈希码
}
}
运行结果如下(错误不用管)
结论:我们可以看出VM判断类是否相同的依据时是否为同一个类加载器加载的相同的.class文件,也就是说,我new 了两个同一个类加载器对象,它门的哈希码不同,肯定不是同一个类加载器,然后我用这两个不同的类加载器加载一个相同的.class文件所生成的类也不是同一个类。