目录
反射(reflect)
反射:在运行时探究和使用编译时未知的类。
很明显在这句话中有两个时:运行时、编译时。根据我们的开发流程,都是先书写一个类,然后在代码里面产生它的对象,调用它的方法或属性,然后编译!编译结束以后,再运行,就能看到相应的效果---所以这是一个先编译后运行的流程。
但是反射这句话把整个过程给颠倒了,我们在是运行起来以后,才知道我们要产生哪个类的对象,调用它的哪个方法或属性。
举个例子,比如:IDEA这个软件就是用Java写的,它在运行起来以后可以探究到我们自己在它运行之后才编译好的Student类里面有哪些属性和行为,然后让我们通过联想的方式可以看到。
这就是Java语言提供的动态性效果。这里的动态性指的是“运行”起来以后,对应的“静态性”不是static而是编译后运行前。
编程语言分为:动态语言和静态语言。动态语言可以在程序运行起来之后,给它的类增加或修改或减少属性和行为。当然,Java作为静态语言做不到。 Java虽然不是动态语言,但是它具有一定的动态性。这个动态性就体现在“反射”上,虽然它没有办法在运行期给类增加/修改/删除成员内容,但是它可以探究和使用,而这就是反射提供的效果。
Java中的反射相关知识点
Java的工作流程:
1、先编写Java源文件;
2、然后编译Java原文件为class文件;
3、运行,而运行又分为3步:
3-1、加载
3-2、校验
3-3、解释执行
“反射”在这几个过程中和“加载”的关系很密切,所以我们要再深入“加载”来聊一聊。
1、加载谁? 2、谁加载? 3、加载成什么?
第一个问题:加载的是class文件。细节在于:一个Java类就会编译成一篇class文件,那么加载class文件就是在加载这个类,所以这个过程也叫做“类加载”;
第二个问题:在JVM当中,提供了一种叫做“类加载器”的东西,来完成加载这个动作。“类加载器”也叫做ClassLoader,它是JDK当中已经写好了的,提供给JVM用来做加载动作的。 Java在这里还专门设计了一个叫做“双亲委托模型”的东西。Java的类加载提供了三种加载器,如果再加上我们自定义的,那就是4种:
BootstrapClassLoader、ExtClassLoader、ApplicationClassLoad、最后自定义的。
在加载的过程中: BootstrapClassLoader负责加载的是JDK中的那些最基本常用的类; ExtClassLoader负责加载的是jre/lib/ext这个包里面的常用类; ApplicationClassLoader是加载我们在应用层面自己写的类。
加载的方式是:向上询问,向下负责。
第三个问题:class文件是一篇字节码文件,那么到了内存当中,它是什么形式呢?它被加载成了一个对象。这个对象的类型是Class类型的,所以也被叫做Class对象。
Class对象是专门用来装一个类的内容信息的。一个类只需要一个Class对象。所以,在这句代码中其实产生了两个对象:
Student stu = new Student();
1、new出来的,交给stu去指向的,是Student的“实例对象”;表示一个学生对象,里面是该学生的数据和它可以执行的行为;
2、但是实例对象是运行期产生的,而在它之前需要先加载Student这个类,那么就会产生一个Class对象,在它当中装的是Student类的内容信息,包括:这个类的名字、父类是谁、接口有哪些、放在哪个包、有什么属性、构造、行为.....所以这个Class对象又被叫做“类模版对象”。
3、类模版对象和实例对象的关系
3-1、必须先有模版对象,然后才能有实例对象;
3-2、模版对象只需要1个,实例对象可以根据要求产生无数个。
3-3、理论上,模版对象是JVM使用的,用来记载这个类的信息;而实例对象是我们的应用程序用的,用来装载数据和具体调用方法完成功能实现的。
反射在做什么事情呢?反射其实就是通过获取这个类的Class对象,我们就能够知道这个类的信息(探究),拿到这些信息以后,比如:拿到它的构造方法的信息,我们就可以产生实例对象了;拿到它的属性信息,我们就可以给属性进行赋值取值了;这就是所谓的“使用”。当然前提条件是获取它的Class对象。
反射的基本操作步骤:
-
获取一个类的Class对象;
-
通过Class对象完成一个类的信息的探究。主要是探究3个东西:构造、属性和行为;
-
探究到以后,就可以使用这些信息了。探究到构造就可以产生实例对象;探究到属性就可以对属性赋值取值;探究到方法就可以调用。
获取一个Class对象的反射API
方式1、类型名.class
注意 : 所有的类型(包括void)都有Class对象; 基本数据类型可以通过自己.class,也能通过自己的包装类.TYPE 获取到该基本类型的Class对象; 不管用哪种方式(包括后面的方式二和方式三)获取Class对象,同一类型的只有一个Class对象。
示例:
//1、可以根据类型名的标识符获取Class对象
// 所有的Java类型都可以通过这种方式获取它的Class对象
Class stuClass = StudentBean.class;//自定义的Java类的Class对象
Class strClass = String.class;
Class interClass = Serializable.class;//接口的Class对象
Class intArrayClass = int[].class;
Class stuArrayClass = StudentBean[].class;
Class intClass = int.class;//基本数据类型的Class对象
Class intClass2 = Integer.TYPE;//也是基本数据类型int的Class对象
/*
在JDK5之前,由于Java要体现基本数据类型只能装数据,而不能有任何"."
出来的内容。所以,在语法设计的时候不允许用"int"点出任何内容。那么要
获取int的Class的对象,就只能通过int对应的包装类来实现。
但是包装类.class得到的是包装类的Class对象;所以又给每个包装类设计
了一个TYPE,用于专门获得对应的基本数据类型的Class对象。
所以:Integer.class和Integer.TYPE得到的是不同的Class对象。
*/
//System.out.println(intClass2 == intClass);//结果为false
Class voidClass = void.class;//甚至连void也有Class对象。
方式2、实例对象.getClass()
注意 :getClass()来自于Object,所以只有Objet的子类才可以通过这种方式获取;接口、基本数据类型、void都不能通过这种方式获取它们的Class对象。
示例:
//2、通过实例对象的getClass()方法获取到Class对象
StudentBean stu = new StudentBean("zhang3",18);//StudentBean的实例对象
Class stuClass1 = stu.getClass();
Class stuClass = StudentBean.class
// System.out.println(stuClass == stuClass1);//结果为true
Class strClass1 = "hello".getClass();
Class strClass2 = "world".getClass();
int[] intArray = {1,3,5,7,9};
Class intArrayClass1 = intArray.getClass();
方式3、Class.forName(类型的字符串名称)
注意 :这种方式是需要做异常处理的,因为有可能这个字符串对应的类型找不到,所以forName方法抛出了一个编译时异常ClassNotFoundException。 这种方式只能获取类、接口、数组的Class对象,不支持基本数据类型和void。 其中数组的字符串类名有古怪:
Student[] --> "[Lcom.lovo.bean.Student"
Serializable[] --> "[Ljava.io.Serializable"
byte[] "[B"
short[] "[S"
int[] "[I"
long[] "[J" -- 特殊
float[] "[F"
double[] "[D"
char[] "[C"
boolean[] "[Z" -- 特殊
示例:
//3、通过字符串形式的类型名获取Class对象
try {
Class stuClass3 = Class.forName("com.project.bean.StudentBean");
Class strClass4 = Class.forName("java.lang.String");
Class interClass2 = Class.forName("java.io.Serializable");
Class intArrayClass2 = Class.forName("[I");
Class intClass1 = Class.forName("void");
int[] intArray = {1,2,3,4,5};
Class intArrayClass1 = intArray.getClass;
System.out.println(intArrayClass2 == intArrayClass1);//结果为true
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
最后,我们需要对比一下这三种方式: 方式1 是可以获取所有的Java类型; 方式2 只能获取Object的子类类型; 方式3 可以获取所有的引用数据类型。
除了方式3,其他两种方式都没有“动态性”。
示例:
String str = new Scanner(System.in).next();
try {
/*
Class.forName在编译的时候并不知道要获取谁的Class对象;
JVM也不知道要创建谁的Class对象。
而是运行起来以后,根据传入的str的值,才能确定要加载谁,
然后把谁的Class对象交给c。
这才是有动态性的反射代码。
*/
Class c = Class.forName(str);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
探究一个Class对象的反射API
探究类的基本信息:
String className = stuClass.getName();//得到类的全名--类的限定名
System.out.println(className);
className = stuClass.getSimpleName();//得到类的简单名
System.out.println(className);
Class superClass = stuClass.getSuperclass();//得到父类的Class对象
String superName = superClass.getName();
System.out.println("父类名:" + superName);
Class[] interClasses = stuClass.getInterfaces();//得到这个类实现的接口的Class对象
System.out.println("接口名:");
for(Class interC : interClasses){
System.out.println(interC.getName());
}
Package aPackage = stuClass.getPackage();//得到类所在的包信息
System.out.println("所在包:" + aPackage.getName());
探究构造方法
getConstructors() -- 获取所有的公共构造
getDeclaredConstructors() -- 获取所有声明的构造
这两个方法都是返回的Constructor[]。
getConstructor(参数的class对象) -- 获取指定的公共构造 getDeclaredConstructor(参数的class对象) -- 获取指定的声明构造
示例:
Constructor[] allPublicCons = stuClass.getConstructors();//获取所有的公共构造
Constructor[] allCons = stuClass.getDeclaredConstructors();//获取所有的构造
Constructor publicCon = stuClass.getConstructor(String.class,int.class);//获取指定的某一 个公共构造
Constructor theCon = stuClass.getDeclaredConstructor(String.class);//获取某个指定的声明构造与访问修饰符无关
for(Constructor con : allCons){
String conName = con.getName();//构造方法的名字是"类的限定名"
int modNum = con.getModifiers();//构造方法的修饰符
String modStr = Modifier.toString(modNum);
Parameter[] params = con.getParameters();//构造方法的参数
String strParams = "";
for(int i = 0; i < params.length; i++){
String parmStr = params[i].getType().getName();//得到参数的类型
String parmName = params[i].getName();
strParams += parmStr + " " + parmName;
if(i != params.length - 1){
strParams +=",";
}
}
System.out.println(modStr + " " + conName + "(" + strParams + ")");
}
探究属性
getFields() -- 获取所有的公共属性
getDeclaredFields() -- 获取所有声明的属性
getField(属性名) -- 获取某个指定的公共属性
getDeclaredField(属性名) -- 获取某个指定的申明属性
示例:
Field[] allPublicFields = stuClass.getFields();//获取所有的公共属性
Field[] allFields = stuClass.getDeclaredFields();//获取所有的申明属性
Field publicF = stuClass.getField("id");//获取指定的公共属性
Field theF = stuClass.getDeclaredField("CLASSNAME");//获取某一个指定的申明属性
探究方法
getMethods() -- 获取所有的公共方法
getDeclaredMethods() -- 获取所有声明的方法
getMethod(方法名,参数的class对象) -- 获取某个指定的公共方法
getDeclaredMethod(方法名,参数的class对象) -- 获取某个指定的声明方法
示例:
Method theM = stuClass.getDeclaredMethod("setName",String.class);
总结一下
这里的探究告诉了我们,在一个类的Class对象里面封装好了这个类的所有信息(只要你在类的代码里面声明的东西,都可以获取到)。 当然对我们最有意义的,最常用的,就是获取构造、属性和方法的那3组12个get。不要死记,其实就是4个单词和单复数的区别。
使用探究到的信息的反射API
探究到构造,就要产生实例对象
Constructor对象有一个叫做newInstance的方法,通过这个方法可以产生实例对象。
注意:
1、获取到的Constructor对象的参数要和newInstance方法中传递的参数匹配!!!--- 这个其实就是形参和实参的匹配关系。
2、由于我们通过getDeclaredConstructor是可以找到这个类所有的构造方法,但是调用newInstance的时候还是要看该构造的访问修饰符。如果调用处与定义的时候规定的访问权限不匹配,仍然不能产生对象成功。会报:IllegalAccessException。
示例:
//先将StudentBean中的公共无参构造访问修饰符设置为private
Class stuClass = StudentBean.class;
//Constructor stuCon1 = stuClass.getDeclaredConstructor();//获取私有无参构造
//StudentBean stu1 = (StudentBean) stuCon1.newInstance();//此处调用这个构造,必然会报异常
Constructor stuCon2 = stuClass.getConstructor(String.class, int.class);
StudentBean stu2 = (StudentBean) stuCon2.newInstance("zhang3",28);//1、尊重访问修饰符
//2、要与获取到的构造匹配参数
探究到属性,就要给这个属性赋值取值
Field对象身上有一个get()/set()的方法,可以给这个属性进行取值/赋值。当然仍然有前提条件,要遵守这个属性定义的访问修饰符。
示例:
//普通属性
Field f = stuClass.getField("id");
int stuID = (int)f.get(stu2);//取值
f.set(stu2,18);//赋值
//静态属性
Field f2 = stuClass.getField("CLASSNAME");
f2.set(null,"公共一班");
探究到方法,就要调用这个方法
Method对象身上有一个invoke()的方法,可以调用这个method。当然仍然有前提条件,要遵守这个方法定义的访问修饰符。
示例:
Method m = stuClass.getMethod("setAge", int.class);
m.invoke(stu2,23);
Method m2 = stuClass.getMethod("getName");
String returnName = (String)m2.invoke(stu2);
System.out.println(returnName);
总结一下
Constructor的newInstance、Method的invoke、Field的get/set。在操作的时候要注意以下几点: 1、调用处的位置要和目标定义的访问修饰符保持一致;
2、Method和Field的操作要注意和实例对象的绑定关系。