1.首先咱们来聊一聊什么叫类加载器
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中,具体来说是加载.class文件到jvm内存。java源代码(.java文件)在经过java编译器编译(javac 指令)之后会生成一个或多个的.class文件,当需要生成该对象的实例时,虚拟机会去常量池查找该类是否被加载,如果没有被加载,就会调用类加载器来将.class文件中的二进制流加载到内存中。而加载二进制流的工具 (实现这一动作的代码块)就是我们所说的类加载器。
2.类加载器的作用:
从上面可以知道,类加载器干的活就是将对应类的.class文件中的二进制流加载到内存空间。
注意:这里所说的加载到内存空间只是将二进制流写入内存,还并没有将二进制流的存储结构解析并写入方法区,这一步操作是在 【类加载】 【验证阶段】 的 【文件格式验证阶段】才完成。
3.类与类加载器之间的关系
对于任意的一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中唯一性
每一个类加载器,都拥有一个独立的类名称空间 (这也是为什么每个类的初始化只会执行一次的原因)
通俗的来讲:比较两个类是否“相等”,只有这两个类由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个.class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必然不相等。
那么问题来了:java如何判断两个对象是否"相等"?
分析这个问题前先来复习两个问题,对象的比较的 == 与 equals
大家都知道用 ==来比较是直接判断当前比较的两个对象是不是同一个对象,也就是当前两个对象的指针指向的是不是同一片内存区域,简单来说:== 的比较方式判断的是 内存地址 也就是 是不是同一个对象。因为虚拟机中在同一时刻一个内存块只能存放一个对象,也就是说内存地址是唯一的。
equals的比较方法在继承自Object的子类没有重写equals方法之前调用的是父类也就是Object的equals方法。而Object的equals方法实现使用的就是 == 判断两个对象是不是同一个对象。
源码:
public boolean equals(Object obj) { // Object的equals方法实现
return ( this == obj);
}
如果子类重写了equals方法,则调用的是子类自己的实现逻辑,我们一般把equals比较的结果当成是对象的值的比较,也就是 equals比较的是对象的 值
hashcode与equals的关系
Object中有个 hashcode方法,这个方法默认返回的是内存地址生成的一个 int 值(虚拟机的实现不同,可能有出入),也就是说默认的hashcode是唯一的(因为内存地址唯一)
但是如果重写了Object的hashcode方法,那么hashcode的生成规则是由子类自己决定,与内存地址就无关了(只是与当前对象的内存地址无关了,其实质还是通过对象的某些字段的内存地址生成的int值)。
所以对象是否相等这里要分成三种情况来讨论:
1.没有重写 对象的equals 与 hashcode 方法
如果对象没有重写equals方法,那么比较的是两个对象的 hashcode(内存地址根据一定的规则生成),实质上也就是对象的内存地址,更通俗一点就是两个 是不是同一个对象
此处 hashcode 不相等,像个对象必然不相等 ,反之亦然
2.重写了 equals方法,但是没有重写hashcode方法
如果重写了equals方法,那么我们就是根据 自己定义的equals方法 比较对象的值。由于没有重写hashcode方法,那么hashcode的生成规则依旧是根据 内存按照一定规则生成
此处分为两种情况:
1.如果两个对象的equals 方法为真,但他们的 hashcode不一定相同.
2.如果两个对象的hashcode相同,那么他们是同一个对象,当然equals会为真
3.重写了equals 与 hashcode 方法
重写了hashcode方法,那么hashcode就是根据自己定义的规则生成,与内存地址无关了
如果重写了equals与hashcode方法那么比较的规则就是与Object的基本一致,唯一不同的是,调用的是子类中的equals与hashcode方法
此处如果hashcode相同,那么两个对象比较的euqls方法为真,反之亦然
所以在Object的equals方法的注释里有这么一段话:
* Note that it is generally necessary to override the {@code hashCode}
* method whenever this method is overridden, so as to maintain the
* general contract for the {@code hashCode} method, which states
* that equal objects must have equal hash codes.
大概意思也就是:为了维护 hashcode 方法的一般合约,即:相同的对象必须要具备相同的哈希值,在重写了equals 方法后 有必要 重写 hashcode方法。注意这里是有必要,没有一定 强制重写。
重写了equals却没有重写hashcode,一般不会有什么问题,但要是要用到hashcode作为比较的条件的时候才会有问题,比方说对象作为map的Key等。
hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable。
扯远了,言归正传。上面讲到了类加载器的原理与作用,下面来分析一下类加载器的类型。
4.类加载器的分类
从java虚拟机的角度来讲,只分为两种类加载器,一种叫做启动类加载器,这个加载器有C++实现,是虚拟机的一部分。另外一种就是其他的类加载器,这些类都是有java语言实现,独立于虚拟机之外,并且都有一个共同点:继承自抽象类java.lang.ClassLoader。
从java开发人员的角度来看,分的更加细致,绝大部分的java程序都会提供以下三种系统类加载器。
1)启动类加载器(Bootstrap ClassLoader 引导类加载器),这个类负责将放在<JAVA_HOME>\lib目录下,并且是虚拟机识别的(仅仅按照文件名识别,名字不符合的类库即使在lib目录下也不会被夹在)类库加载到虚拟机内存中。启动类加载器无法被java程序直接引用,如果要把加载请求委派给启动类加载器,那就直接用null代替即可。
2)扩展类加载器(Extension ClassLoader)负责加载<JAVA_HOME>\lib\ext目录下的类库
3)应用程序类加载器(Application ClassLoader)也称为系统类加载器,负责加载用户类路径上所指定的类库,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。通俗的来讲:一般情况下,我们自己写的类是由这个加载器加载。
双亲委派模型 如下图所示:
这个模型执行的总体意思也就是:如果一个类收到了类加载请求,它首先自己不会去加载这个类,而是将这个请求委派给父类的加载器去完成,父类的加载器也不会首先去加载,他会将这个请求委派给自己的父类去完成,如此往复直到顶层的 启动类加载器。只有当父类加载器反馈说自己无法完成这个加载请求时(在它的搜索范围类没有找到所需的类),父类会一层一层往下通知,子类加载器才会自己去加载这个类。
通俗来讲就是这样:
儿子需要一个玩具月亮,就跟老爹讲,老爹懒得理他,就跟自己的老爹说,你孙子要个月亮,爷爷一听孙子要个月亮,就在自己力所能及的范围内搜索,结果发现自己找不到,然后爷爷就对爸爸说,儿子,我找不到孙子要的月亮,还是你自己来找吧,于是爸爸也在自己的搜索范围类搜索,发现自己也找不到这个月亮,于是也对儿子说,儿子,你老子找不到这个月亮,这个事儿呀还是得你自己来,儿子一听你们都找不到那还是我自己来吧,自己找找找呀找到了,拿着月亮玩去了,要是找遍了没找到,儿子就躲一边哭(报找不到类的异常)
这个称之为双亲委派模型,这个模型的作用是java类随着他得类加载器一起具备了一种带有优先层次的关系。例如类java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给顶端的启动类加载器去加载,因此Object类在程序的各种类加载环境中都是同一个类。相反,如果不是用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个java.lang.Object的类,并放在classPath中,那么系统中将出现多个不同的Object类,java体系中最基础的行为也无法保证。
双亲委派模型对于保证java程序的稳定运行很重要,但实现对相当简单逻辑很清楚:先检查类是否被加载过,若没有就调用父加载器的loadClass方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载器加载失败,抛出异常,再调用自己的findClass方法进行加载。
双亲委派模型中的父子关系不是由继承关系实现的,而是以组合关系来复用父加载器的代码
双亲委派模型被广泛应用于之后几乎所有的java程序,但却并不是一个强制的模型,而是设计者推荐给开发者的一种类加载实现方式