回顾
java之前我接触反射这个知识,是在大话设计中的抽象工厂模式里,通过反射+配置文件来优化抽象工厂提高其应对需求变更的灵活性。当时对于反射的认知仅仅是它是一种技术,一种实例化对象的技术,一种实例化对象不依赖于写死的代码的技术。简单的说就是,它是一种可以摆脱用NEW去实例化对象,显然它应付与对象变换的能力是强大的。其实,反射不论在什么语言里都是一样的,只不过实现的手段不一样而已。之前对代理模式的深入学习过程中又遇到了反射,所以决定这次要一探究竟。
定义
Java可以在运行时加载、探知、使用编译期间完全未知的class。再简单一点说就是java可以在运行时获得任一一个类的信息、构建类的Class对象(强调:不是该类的对象,是后面提高的类类型)、生成类实例、调用methods。这里类的信息包括它的基类、所实现的接口、方法等。个人觉得这里比较难理解的是“编译期间完全未知”。所以,特别解释一下。如下的代码。首先,这个Note类是不存在的,也就是说这段代码有错。分别执行两个方法就可以看出分别了,其实方法2编译时就无法通过,提示类是不能识别的类型,因为本来就不存在这个类。而方法1则时可以通过编译,执行时能打印“方法执行中”。但是实例化时会报空指针的错误。这两种不同时刻产生的错误就说明了“编译期间完全未知”这个说法。
- public class Client {
- public static void main(String[] args)
- throws InstantiationException, IllegalAccessException, ClassNotFoundException{
- System.out.println("方法执行中!"); //打印说明进入该方法
- //方法1:通过反射实例化类Note
- Class c=Class.forName("com.zjjReflect.Note");
- Object o=c.newInstance();
- //方法2:直接实例化类Note
- //Object o=new Note();
- }
- }
public class Client { public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException{ System.out.println("方法执行中!"); //打印说明进入该方法 //方法1:通过反射实例化类Note Class c=Class.forName("com.zjjReflect.Note"); Object o=c.newInstance(); //方法2:直接实例化类Note //Object o=new Note(); } }
RTTI
RTTI是Run-Time Type Information 的缩写,意思是运行时类型信息。,RTTI提供了运行时确定对象类型的方法。但是,RTTI并不是一种技术,应该是一种概念。因为不同的语言对RTTI的实现形式是不一样的。简单的说就是在程序运行时去获得一个对象所对应的类的信息。这么说有点模糊,还是结合RTTI在某种语言里的实现来理解好一些。Java 中就是主要有:类型转换、类类型Class、instanceof体现了RTTI。类类型java.lang.Class
Class是所有类和接口的抽象,比如类的名字、类实现的接口、方法、属性等等。你可以通过某个对象的Class对象来获取类的信息。这里不做详细介绍,后面有单独的博客补充。Class loader
类加载的过程就是将.class加载到内存中。这里为什么要提到classloader呢?因为反射的过程使用到了Classloader,并且不同的类需要相对应的Classloader来加载。也就是说Classloader是和类是配对工作的,然后反射的特点却是在运行时才会知道类的信息所以我们也要对这部分的内容作个了解。也是为了能更好、更深刻的对反射理解。Java的Classloader有四种分别为:
bootstrap classloader :引导(也称为原始)类加载器。
extension classloader :扩展类加载器。
Application ClassLoader:应用程序类加载器。
User Defined ClassLoader:自定义类加载器。
总结:在程序运行时通过类类型Class获得目标类的信息,然后在类信息的基础上使用相对应的类加载器加载到内存中,再然后对这个类中进行实例化,方法调用等的使用的整个过程。就是反射的详细的说法!!
简介
注意:数组类的 Class 对象不是由类加载器创建的,而是由 Java 运行时根据需要自动创建。数组类的类加载器由 Class.getClassLoader() 返回,该加载器与其元素类型的类加载器是相同的;如果该元素类型是基本类型,则该数组类没有类加载器。
分类
Bootstrap ClassLoader:是用C++编写的,是JVM的内置加载器,它的名字是null。它用来加载核心类库,即在lib下的类库。做个实验,首先,String类肯定是java的核心类,那我们就以它为例来看看:
- public static void main(String[] args){
- String a="x";
- System.out.println(a.getClass().getClassLoader());
- }
public static void main(String[] args){ String a="x"; System.out.println(a.getClass().getClassLoader()); }
Extension ClassLoader:加载lib/ext下的类库。
App ClassLoader:加载Classpath里的类库。
层次关系
我们这里可以在做一个实验,来体会一下这个层次关系。代码如下:
- public static void main(String[] args){
- ClassLoader c =TestClassLoader.class.getClassLoader();
- do {
- System.out.println(c.getClass().getName());
- c=c.getParent();
- }while(c!=null);
- }
- }
public static void main(String[] args){ ClassLoader c =TestClassLoader.class.getClassLoader(); do { System.out.println(c.getClass().getName()); c=c.getParent(); }while(c!=null); } }
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
双亲加载机制
很绕啊,文字说不清楚还是上图吧!
双亲加载机制可以一定程度上保证安全性,因为只要顶层ClassLoader能加载的东西就一定不会让下层的ClassLoader有机会加载。也就保证了有些自定义的带有破坏性的类不会被加载到Jvm核心中。
结语:ClassLoader相对难理解的地方一个是,对象将的层次关系和父加载器。另一个是双亲加载机制。这里提供一个视频供大家参考。
概念
Class类是所有类(注意是对象)的共有信息的抽象,比如该类实现的接口、对应的加载器、类名等等。一句话,类类型保存了每个类所对应的类型信息。每一个类都有一个Class对象,这个对象在类被加载后由JVM自动构造。也是由JVM管理的,Class类是没有公共的构造方法的。Class对象对于类来说就像是,DNA对于每个人,里面有你的一切生物信息。java中可以通过Class来取得类的实例,也许将来的将来通过你的DNA也能得到你的另一个实例。科幻电影里是已经实现了。ok,概念应该有个初步的认识了。
常用方法
方法的介绍本来不应该这么简单,但是发现一句两句的说不清楚,并且对于Java的理解有很好的帮助。所以临时决定这部分单独的写一篇博客。这里就简单的列几个,之前用过的方法。
- forName:返回与带有给定字符串名的类或接口相关联的 Class 对象。
- getName():一个Class对象描述了一个特定类的属性,Class类中最常用的方法getName以 String 的形式返回此 Class 对象所表示的实体(类、接口、数组类、基本类型或 void)名称。
- newInstance():创建Class对象描述的类型的新实例。newInstance()方法调用默认构造器(无参数构造器)初始化新建对象。
- getClassLoader():返回该类的类加载器。
- getInterfaces():确定此对象所表示的类或接口实现的接口。
- getComponentType():返回表示数组组件类型的 Class。
- getSuperclass():返回表示此 Class 所表示的实体(类、接口、基本类型或 void)的超类的 Class对象
- isArray():判定此 Class 对象是否表示一个数组类。
怎么得到
获得Class对象的方法有三种(1)利用Object.getClass()方法获取该对象的Class实例;
(2)使用Class.forName()静态方法,用类的名字获取一个Class实例
(3)运用类的.class的方式来获取Class实例,对于基本数据类型的封装类,还可以采用.TYPE来获取相对应的基本数据类型的Class实例
这里需要注意的是虚拟机只会产生一份字节码, 用这份字节码可以产生多个实例对象。也就是说Class对象只会有一个。看如下代码:
测试类
- public class Test {
- static {
- System.out.println("静态初始化");
- }
- {
- System.out.println("非静态初始化");
- }
- }
public class Test { static { System.out.println("静态初始化"); } { System.out.println("非静态初始化"); } }客户端
- public class client {
- public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
- //方法1
- Class c=Class.forName("com.zjj.ClassTest.Test");
- //方法2
- c=Test.class;
- //方法3
- Test t=new Test();
- c=t.getClass();
- Test t2=new Test();
- }
- }
public class client { public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{ //方法1 Class c=Class.forName("com.zjj.ClassTest.Test"); //方法2 c=Test.class; //方法3 Test t=new Test(); c=t.getClass(); Test t2=new Test(); } }输出结果为:
静态初始化
非静态初始化
非静态初始化
大家知道静态初始化方法是在类加载的时候执行的,非静态初始化方法是在类被实例化的时候执行的。而输出结果只打印了一次“静态初始化”,这就说明三次得到的Class对象都是同一个。
也就是说,在运行期间,如果我们要产生某个类的对象或者的得到某个类的Class对象,Java虚拟机(JVM)会检查该类型的Class对象是否已被加载。如果没有被加载,JVM会根据类的名称找到.class文件并加载它。一旦某个类型的Class对象已被加载到内存,就可以用它来产生该类型的所有对象
结语
本篇总结:至此,应该可以理解了Class也是一个类,只不过它是所有类的一个抽象,名字又和我们所知道的Class一样容易造成混淆。总的来说,每一个类都有对应的一个Class对象来保存这个类的信息,这个Class对象由JVM构造和管理。Class对象的存在是Java反射的基础。反射机制总结:反射机制是Java的一个重要的内容,为Java提供了运行时加载类的能力,也就是动态性。Class是信息提供者,Class Loader是加载工具,二者都是反射机制最基础的部分。那么所谓的反射就是解除耦合,方式就是通过Class取得未知类的信息,而后实例化。当然Class Loader的所做的工作是隐藏的,是Class对象去调用的。所以无需显示的自己调用。
反射机制这几篇博客写下来发现涉及到Java类的加载机制,这部分的内容也比较独立的一部分,因此单另一篇来写。在JAVA中任何的类都是需要加载到JVM中才能运行的。之前Class Loader介绍了类的加载机制,那么这里要说的是不同加载方式之间的对比,好能对JAVA类的实例化过程有更深刻的体会。
new和Class.newInstance
我们说代码里出现new关键字意味着对于可能变动的代码,耦合过高了。遇到这种情况我们会用反射机制来去除new关键字,这在代理模式里我们见过了。实际上也就是用了Class.newInstance来代替。这说明这两种方式都可以得到相同的对象实例,但是它们之间存在区别,耦合度不同。
实际上在理解上我们可以认为,Class.newInstanc方式来实例化对象是对new关键字的拆分成两步了。因为,Class.newInstance的使用是有前提的,要保证类已经加载到JVM中,并且已经链接。看如下代码:
- <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
- //从当前线程取得正在运行的加载器
- ClassLoader cl=Thread.currentThread().getContextClassLoader();
- cl.loadClass("com.zjj.ClassTest.Test"); //加载测试类到JVM
- Class c2=cl.getClass(); //得到类的Class对象
- c2.newInstance(); //实例化对象
- }
- }</span></span>
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{ //从当前线程取得正在运行的加载器 ClassLoader cl=Thread.currentThread().getContextClassLoader(); cl.loadClass("com.zjj.ClassTest.Test"); //加载测试类到JVM Class c2=cl.getClass(); //得到类的Class对象 c2.newInstance(); //实例化对象 } }</span></span>
这里不用Class.forName来得到Class对象是为了保证类被加载了但是没有被链接。 这段代码看着貌似没什么错,编译也没有问题,但是运行的时候就出错了。也就是说通过如上方法加载的类是没有被链接的,因此newInstance方法无法执行。
前面说理解上可以简单的认为是通过Class.Instance方式是new拆分的两步,但是事实上new要比Class.Instance做的多。Class.Instance方法只能访问无参数的构造函数,new则都可以访问。建立一个有两个构造函数的测试类,看客户端调用代码:
- <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
- Class c=Class.forName("com.zjj.ClassTest.Test");
- c.newInstance();
- new Test("ni");
- }
- }</span></span>
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{ Class c=Class.forName("com.zjj.ClassTest.Test"); c.newInstance(); new Test("ni"); } }</span></span>
输出结果为:
无参数的构造函数
带参数的构造函数
如果在newInstance中传入参数去调用带参数的构造函数的话是会报错的,无法通过编译。相对来说newInstance是弱类型,new是强类型。
Class.forName和classLoad.loadClass
讲这两个的区别之前我们先要了解,JVM会执行静态代码段,要记住一个概念,静态代码是和class绑定的,class装载成功就表示执行了静态代码了,以后也就不会再走这段静态代码了。 也就是说静态代码段是只会执行一次的,在类被加载的时候。另外我们还需要知道,类的加载过程分为装载、连接、初始化。还有就是,JVM遇到类请求时它会先检查内存中是否存在,如果不存在则去加载,存在则返回已存在的Class对象。
那么这两个方法的区别就在于执行的这三个过程不一样。forName有两个函数(多态),三个参数时forName(String className, boolean initialize, ClassLoader loader)第二个参数为True时则类会链接,会初始化。为False时,如果原来不存在则一定不会连接和初始化,如果原来存在被连接的Class对象,则返回该对象但是依然不会初始化。单参数时,默认initialize是为True的。
loadClass也是多态loadClass(String name)单参数时, resolve=false。如果该类已经被该类装载器所装载,那么,返回这个已经被装载的类型的Class的实例,否则,就用这个自定义的类装载器来装载这个class,这时不知道是否被连接。绝对不会被初始化!这时唯一可以保证的是,这个类被装载了。但是不知道这个类是不是被连接和初始化了。
loadClass(String name, boolean resolve)resolve=true时,则保证已经装载,而且已经连接了。 resolve=falses时,则仅仅是去装载这个类,不关心是否连接了,所以此时可能被连接了,也可能没有被连接。下面通过测试来验证以上说的内容,代码如下:
Test类:
- <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public class Test {
- static {
- System.out.println("静态初始化");
- }
- public Test(){
- System.out.println("无参数的构造函数");
- }
- public Test(String str){
- System.out.println("带参数的构造函数");
- }
- {
- System.out.println("非静态初始化");
- }
- }</span></span>
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public class Test { static { System.out.println("静态初始化"); } public Test(){ System.out.println("无参数的构造函数"); } public Test(String str){ System.out.println("带参数的构造函数"); } { System.out.println("非静态初始化"); } }</span></span>
测试一:客户端调用代码
- <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
- Class c=Class.forName("com.zjj.ClassTest.Test");
- }
- }</span></span>
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{ Class c=Class.forName("com.zjj.ClassTest.Test"); } }</span></span>
输出结果为:静态初始化
说明:Class.forName时类执行了装载、连接、初始化三个步骤。
测试二:客户端代码改为
- <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
- ClassLoader cl=Thread.currentThread().getContextClassLoader();
- Class c=Class.forName("com.zjj.ClassTest.Test", false, cl);
- }
- }</span></span>
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{ ClassLoader cl=Thread.currentThread().getContextClassLoader(); Class c=Class.forName("com.zjj.ClassTest.Test", false, cl); } }</span></span>
输出结果为:initialize=true时输出,静态初始化。为false时没有输出
说明:为true时类执行了装载、连接、初始化三个步骤。为false时没有初始化,为知是不是连接。
测试三:客户端代码改为
- <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
- ClassLoader cl=Thread.currentThread().getContextClassLoader();
- Class c=Class.forName("com.zjj.ClassTest.Test", false, cl);
- c.newInstance();
- }
- }</span></span>
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{ ClassLoader cl=Thread.currentThread().getContextClassLoader(); Class c=Class.forName("com.zjj.ClassTest.Test", false, cl); c.newInstance(); } }</span></span>
输出结果为:
静态初始化
非静态初始化
无参数的构造函数
说明:为了保证JVM中不存在之前加载过的类,特地清理了JVM内存。但是输出结果不变,说明为false时执行了装载和链接,否则newInstance是无法执行的(前面说过了newInstance的执行条件)。但是资料说可能还存在不连接的情况!!有待考证。
测试四:客户端代码改为
- <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
- Class c=Class.forName("com.zjj.ClassTest.Test");
- ClassLoader cl=Thread.currentThread().getContextClassLoader();
- Class c=Class.forName("com.zjj.ClassTest.Test", true, cl);
- }
- }</span></span>
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{ Class c=Class.forName("com.zjj.ClassTest.Test"); ClassLoader cl=Thread.currentThread().getContextClassLoader(); Class c=Class.forName("com.zjj.ClassTest.Test", true, cl); } }</span></span>
输出结果为:静态初始化
说明:如果原来存在加载过的类,那么第二次执行加载请求时返回存在的。因为,静态初始化只执行了一次。
测试五:客户端代码改为
- <span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
- //从当前线程取得正在运行的加载器
- ClassLoader cl=Thread.currentThread().getContextClassLoader();
- cl.loadClass("com.zjj.ClassTest.Test"); //加载测试类到JVM
- Class c2=cl.loadClass("com.zjj.ClassTest.Test").getClass(); //得到类的Class对象
- c2.newInstance(); //实例化对象
- }
- }</span></span>
<span style="font-family:FangSong_GB2312;font-size:18px;"><span style="font-family:FangSong_GB2312;">public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{ //从当前线程取得正在运行的加载器 ClassLoader cl=Thread.currentThread().getContextClassLoader(); cl.loadClass("com.zjj.ClassTest.Test"); //加载测试类到JVM Class c2=cl.loadClass("com.zjj.ClassTest.Test").getClass(); //得到类的Class对象 c2.newInstance(); //实例化对象 } }</span></span>
输出结果:报错
说明:此时loadClass方法加载到内存中的类是未连接的,当然不会初始化。因此也就没有“静态初始化”的输出。
测试六:不知道为什么没有发现代码中的ClassLoader存在两个参数的loadClass方法。
总结:至此方法对比结束,这篇博客主要是更细致的了解了JVM加载类的过程和不同方式之间的区别。其实际上只是封装的程度不一样,也就是方法的粒度的差别。当然,有一点内容还没有通过自己的测试得到验证,可能是我的方法不对或者是资料有问题。权且记下这个问题!