由之前动态代理的学习再次接触到反射这个知识点,第二次接触了所以做了一些稍微深入的了解。那么,对于反射这部分的内容我打算分三篇博客来总结。本篇博客先对反射做一个大概的了解,包括反射有关的RTTI、定义的理解以及涉及到的其他知识的简介。
回顾
java之前我接触反射这个知识,是在大话设计中的抽象工厂模式里,通过反射+配置文件来优化抽象工厂提高其应对需求变更的灵活性。当时对于反射的认知仅仅是它是一种技术,一种实例化对象的技术,一种实例化对象不依赖于写死的代码的技术。简单的说就是,它是一种可以摆脱用NEW去实例化对象,显然它应付与对象变换的能力是强大的。
其实,反射不论在什么语言里都是一样的,只不过实现的手段不一样而已。之前对代理模式的深入学习过程中又遇到了反射,所以决定这次要一探究竟。
定义
Java可以在运行时加载、探知、使用编译期间完全未知的class。再简单一点说就是java可以在运行时获得任一一个类的信息、构建类的Class对象(强调:不是该类的对象,是后面提高的类类型)、生成类实例、调用methods。这里类的信息包括它的基类、所实现的接口、方法等。
个人觉得这里比较难理解的是“编译期间完全未知”。所以,特别解释一下。如下的代码。首先,这个Note类是不存在的,也就是说这段代码有错。分别执行两个方法就可以看出分别了,其实方法2编译时就无法通过,提示类是不能识别的类型,因为本来就不存在这个类。而方法1则时可以通过编译,执行时能打印“方法执行中”。但是实例化时会报空指针的错误。这两种不同时刻产生的错误就说明了“编译期间完全未知”这个说法。
[java] view plain copy print ?
- public class Client {
- public static void main(String[] args)
- throws InstantiationException, IllegalAccessException, ClassNotFoundException{
- System.out.println("方法执行中!");
-
- Class c=Class.forName("com.zjjReflect.Note");
- Object o=c.newInstance();
-
-
- }
- }
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获得目标类的信息,然后在类信息的基础上使用相对应的类加载器加载到内存中,再然后对这个类中进行实例化,方法调用等的使用的整个过程。就是反射的详细的说法!!
上面简要的提了一下java反射机制中涉及到的一些相关知识,那么ClassLoader就是其中之一。紧接着就详细的对ClassLoader做一个相对深入的了解。作为了解需要知道的是,其实类类型(Class Class)是和ClassLoader分不开的,因为ClassLoader需要的信息是由它提供的。类类型将在下一篇博客介绍。
简介
ClassLoader是负责加载类的对象,作用是根据Jvm请求提供的类信息,将请求的类加载的内存中或者说加载到Jvm中。另外,每一个类的Class对象(注意Class是类类型)都持有一个对应的ClassLoader的引用。可以通过Class对象的getClassLoader()方法得到。类和它的ClassLoader是对应的,所以类只能通过它对应的ClassLoader加载。
注意:数组类的 Class 对象不是由类加载器创建的,而是由 Java 运行时根据需要自动创建。数组类的类加载器由 Class.getClassLoader() 返回,该加载器与其元素类型的类加载器是相同的;如果该元素类型是基本类型,则该数组类没有类加载器。
分类
JVM在运行时会产生三个ClassLoader,Bootstrap ClassLoader、Extension ClassLoader和App ClassLoader。
Bootstrap ClassLoader:是用C++编写的,是JVM的内置加载器,它的名字是null。它用来加载核心类库,即在lib下的类库。做个实验,首先,String类肯定是java的核心类,那我们就以它为例来看看:
[java] view plain copy print ?
- 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());
}
我们通过代码来获得String加载对应的ClassLoader的名字输出的结果为NULL。
Extension ClassLoader:加载lib/ext下的类库。
App ClassLoader:加载Classpath里的类库。
层次关系
之前我们说过,每一个Class对象都会持有一个对应的ClassLoader的引用。每一个ClassLoader对象也会持有一个Parent ClassLoader的引用。这里需要特别注意的是:这里所指的的Parent ClassLoader不是我们熟悉的继承关系,不是父类!!首先,我们要知道这里说的是ClassLoader对象,也就是说这的Parent ClassLoader其实是一个对象的引用。下面看一张图,了解一下ClassLoader对象之间的层次关系:
我们这里可以在做一个实验,来体会一下这个层次关系。代码如下:
[java] view plain copy print ?
- 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
双亲加载机制
层次关系中我们了解到了很重要的一点:加载器对象之间的引用关系。被引用的对象称之为引用对象的父加载器,可以通过getParent()方法得到。那么双亲加载机制就是基于这种引用的层次关系。即:当一个ClassLoader接到请求时,它不是直接加载对应的类,而是询问它引用的ClassLoader是否能够加载,而这个父ClassLoader则会询问自己的引用的ClassLoader是否加载了该类。只有当所有的父ClassLoader都没有加载该类时,这个最初的ClassLoader才自己去加载申请的类。
很绕啊,文字说不清楚还是上图吧!
双亲加载机制可以一定程度上保证安全性,因为只要顶层ClassLoader能加载的东西就一定不会让下层的ClassLoader有机会加载。也就保证了有些自定义的带有破坏性的类不会被加载到Jvm核心中。
结语:ClassLoader相对难理解的地方一个是,对象将的层次关系和父加载器。另一个是双亲加载机制。这里提供一个视频供大家参考。
为了区别于我们常用的Class,因此给了个中文名类类型。目的就是为了知道此Class非彼Class。前面已经介绍了Class Loader,它的作用是根据提供的信息来加载类到内存中。我之前有提过这个类信息的提供者就是本篇博客要介绍的Class。提醒:一个类的Class对象和它的Instance是不一样的,切记,不然你会混乱的。开始了!
概念
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对象只会有一个。看如下代码:
测试类
[java] view plain copy print ?
- public class Test {
- static {
- System.out.println("静态初始化");
- }
- {
- System.out.println("非静态初始化");
- }
- }
public class Test {
static {
System.out.println("静态初始化");
}
{
System.out.println("非静态初始化");
}
}
客户端
[java] view plain copy print ?
- public class client {
- public static void main(String[] arg) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
-
- Class c=Class.forName("com.zjj.ClassTest.Test");
-
- c=Test.class;
-
- 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中,并且已经链接。看如下代码:
[java] view plain copy print ?
- <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");
- Class c2=cl.getClass();
- 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则都可以访问。建立一个有两个构造函数的测试类,看客户端调用代码:
[java] view plain copy print ?
- <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类:
[java] view plain copy print ?
- <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>
测试一:客户端调用代码
[java] view plain copy print ?
- <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时类执行了装载、连接、初始化三个步骤。
测试二:客户端代码改为
[java] view plain copy print ?
- <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时没有初始化,为知是不是连接。
测试三:客户端代码改为
[java] view plain copy print ?
- <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的执行条件)。但是资料说可能还存在不连接的情况!!有待考证。
测试四:客户端代码改为
[java] view plain copy print ?
- <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>
输出结果为:静态初始化
说明:如果原来存在加载过的类,那么第二次执行加载请求时返回存在的。因为,静态初始化只执行了一次。
测试五:客户端代码改为
[java] view plain copy print ?
- <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");
- Class c2=cl.loadClass("com.zjj.ClassTest.Test").getClass();
- 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加载类的过程和不同方式之间的区别。其实际上只是封装的程度不一样,也就是方法的粒度的差别。当然,有一点内容还没有通过自己的测试得到验证,可能是我的方法不对或者是资料有问题。权且记下这个问题!