前言

在学习Java反射的时候,少不了同“Class.forName 和 ClassLoader”打交道,然而不深究的难以了解他们的区别和联系,以及各自的使用场景。

文章目的:了解他们的区别和联系以及使用场景,便于后期使用时对他们有清晰的认识。

概述

loadClass() 方法获得的 Class 对象只完成了类的加载,后续的初始化等操作均未进行。 使用 Class.forName() 方法获得 Class 对象,完成了类加载过程各个环节,并执行完类初始化工作。

下面就和我一起来深究一下他们的区别和联系吧。

一、首先了解:类的加载方式

1.隐式加载(最常用)

使用 new + (有参/无参)构造方法时,隐式(偷偷的)的调用类加载器,加载对应的类到 JVM 中,并隐式的完成类的初始化等各项工作,便于随时通过类的别名调用方法或属性,是最常见的类加载方式。

Student stu=new Student(100,"zhangsan");
stu.

2.显式加载(主要用在反射)

显式加载类获取到 Class 对象后,需要调用 Class 对象的 newInstance() 方法来生成对象的实例。最常用的方式就是通过 loadClass()、forName()

注意:newInstance()方法在jdk9之后的使用有变化。

java8的写法

//该写法在java9中已被废弃
Person p =  (Person) Class.forName("com.succ.reflect.test.Person").newInstance();

java9的写法

Person p2 =  (Person) Class.forName("com.succ.reflect.test.Person").getDeclaredConstructor().newInstance();//java9的写法

3.隐式加载和显式加载的区别

1.隐式加载能够直接获取对象的实例,而显式加载需要调用 Class 对象的 newInstance() 方法来生成对象的实例。

2.隐式加载能够使用有参的构造函数,而使用 Class 对象的 newInstance() 不支持传入参数,如果想使用有参的构造函数,必须通过反射的方式,来获取到该类的有参构造方法。

3.显式加载常用在反射,使用更为灵活,可以一个代码对应很多个类,根据传入的class类的名称映射为不同的实现类。

二、Class.forName 和 ClassLoader的区别

1.相同点

两者都可以对类进行加载。 对于任意一个类/对象,我们都能够通过反射能够调用这个类的所有属性方法。

2.不同点

抽象类ClassLoader中实现的方法loadClass,loadClass只是加载,不会解析更不会初始化所反射的类。常用于做懒加载,提高加载速度,使用的时候再通过.newInstance()真正去初始化类。 

public class CommWayTest {
	public static void main(String[] args) throws ClassNotFoundException {
		ClassLoader loader = CommWayTest.class.getClassLoader();//注:CommWayTest是当前类的类名
		Class c4 = loader.loadClass("com.succ.demo.Person");
        Person person=(Person)c4.newInstance();//使用的时候,再通过.newInstance()进行初始化
	}
}

Class.forName()加载完毕一个指定类后,会对该类自动进行初始化。常用于加载mysql的jdbc启动。

Class.forName("com.mysql.jdbc.Driver");

3.ClassLoader不解析不初始化类,为什么还要使用它呢?

简单的说,为了提高类的加载速度。 比如:在 Spring IOC (读取Bean配置)时,一般使用的就是 ClassLoader 的 loadClass() 方法来加载。

之所以这样做,是和 Spring IOC 的 Lazy Loading 有关,即延迟加载。Spring IOC 为了加快初始化的速度,大量的使用了延迟加载技术,而使用 ClassLoader 的 loadClass() 方法不需要执行类加载过程中的链接和初始化的步骤,这样做能有效的加快加载速度,把类的初始化工作留到实际使用到这个类的时候才去执行。

三、两者的实现原理 

1.ClassLoader实现原理

ClassLoader就是遵循双亲委派(下文有介绍)模型最终调用启动类加载器的类加载器,实现的功能是“通过一个类的全限定名来获取描述此类的二进制字节流”,获取到类的二进制流后放到JVM中,完成加载工作。

源码如下:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);//可以看到,该public方法,调用的是protect方法loadClass(name, false),入参是false
  }

通过上面源码,可以看到该public方法调用的是protect方法loadClass(name, false),入参是false。

重点记忆这个入参resolve,稍后下面的Class.forName()源码中也有类似的用法,不过入参是true!!!

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
             // 省略加载机制实现代码
            if (resolve) {
                resolveClass(c); 
            }
            return c;
}

综上所述:loadClass是抽象类ClassLoader中实现的方法,从源码来看,当调用ClassLoader.loadClass()方法时,调用的是loadClass(name,false),注意到第二个参数false,可以得知loadClass只是加载类,不会对类进行解析和初始化。 

2.Class.forName()实现原理

先看一下他的源码

@CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

可以看到他的下一级实现方法是forName0,注意它的一个参数true,对应的是initialize(初始化),就是说这个方法加载完类会对类进行初始化。

private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;

同时可以看到,forName0的底层实现有涉及ClassLoader类。  

四、扩展,双亲委派机制

1.什么是双亲委派机制

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

java 某个区域load_java 某个区域load

  

2.为什么要使用双亲委派机制

1.防止重复加载同一个.class文件,通过委托去向上级问,加载过了就不用加载了(相当于提前加载父类)。

2.保证核心.class文件不会被串改(即使篡改也不会加载,即使加载也不会是同一个对象),因为不同加载器加载同一个.class文件得到的也不是同一个class对象,从而保证了class执行安全。

总结

1.两者都可以使用类的全量限定名,通过反射获得实体类。

public class CommWayTest {
	public static void main(String[] args) throws ClassNotFoundException {
		Class aa=Class.forName("com.mysql.jdbc.Driver");
		Class bb=CommWayTest.class.getClassLoader().loadClass("com.mysql.jdbc.Driver");
	}
}

注:如果你对 Class.forName的使用感兴趣,点击进入(从第四章节开始看)。

2.Class.forName完成类的加载后,会继续完成类的初始化,而ClassLoader.loadClass仅仅只会完成类的加载。

3.它们的使用场景略有不同,前者主要用于加载驱动、后台代码的反射等场景,后者主要配合懒加载(主要用于配合加载配置等)。 

4.两者的缺点也十分明显,就是在加载类的时候,只能加载类的无参构造函数,不能直接使用类的有参构造函数。

尾言

在学习过程中,总有帖子避重就轻,不疼不痒,看完了也不知所云,经过反复斟酌推敲,故有此文,当然由于水平有限,文笔拙劣,也可能会出现纰漏,如有发现,请予以指正,共同学习……

附注

猜你可能会对以下内容产生兴趣

 java开发:Java反射的意义价值 | 反射的优缺点 | 反射破坏了封装性为什么还要用