反射机制
反射的定义
能够分析类能力的程序称为反射(reflective),Java提供了一个反射库(reflection library)用来方便编写可以动态操作Java代码的程序
反射能用来做什么(后面依次介绍)
在运行时分析类的能力
在运行时查看对象
实现通用的数组操作代码
利用Method对象
一、CLASS对象
反射中最基础也是的就是要了解Class对象,如何使用以及它的含义。
Java运行时系统始终为所有的对象维护一个运行时的类型标识,这个标识跟踪着每个对象所属的类,虚拟机通过运行时的类型标识信息获取对应的方法执行。
我们可以通过专门的Java类来获取到这些信息,保存这些信息的类就是Class类。
常用的获取Class对象的方法有三种:
1.通过Object类的getClass()方法获取
注:第一个就贴完整代码,后面就贴部分代码了 。
public class MyExample {
public static void main(String[] args) {
//新建一个类的对象
Person person = new Person(1234567890,"张三");
//通过getClass方法获取person对象的信息保存在Class类的实例中
Class cl = person.getClass();
}
}
class Person {
long idCard;
String name;
public Person() {
}
public Person(long idCard, String name) {
this.idCard = idCard;
this.name = name;
}
public long getIdCard() {
return idCard;
}
public void setIdCard(long idCard) {
this.idCard = idCard;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"idCard=" + idCard +
", name='" + name + '\'' +
'}';
}
}
那么我们获取到了Class类的实例对象,它里面有什么信息呢?
最常用的比如说类的名字,可以用getName方法获取。
System.out.println(cl.getName() + " " + person.getName());
可以看到Class实例的名字是带有包名的,那么说明实例中保存的是类的全限定名称,也很好理解,Class的信息本来是给虚拟机使用的,那肯定虚拟机运行程序的时候要能准确找到实例对应的类才行。
2.调用静态方法forName获得类名对应的Class对象
String className = "java.util.Random";
Class cl = Class.forName(className);
这里我获取了一下Random类(jdk中包括的工具类,可以生成随机数),需要注意的是获取Class实例的时候需要写全限定名称,然后这个方法可能会抛出checked exception ( 已检查异常),这里就直接throws了,不多说。
3.直接使用class属性获取
Class c1 = Person.class;
Class c2 = Random.class;
Class c3 = int.class;
需要注意的是,一个Class对象表示的是一个类型,类是一种类型,但是类型不一定是类,比如int类型,int.class也是一个Class类型的对象
还有一点就是Class<>实际上是一个泛型类,不过此处不深究,我们忽略使用就好。
二、利用反射分析类的能力
在java.lang.reflect包下面有三个类:
Field 描述域
Method 描述方法
Constructor 描述构造器
先看一个例子,然后例子中的注释比较全,在例子后面用到的方法也会介绍,与其说一堆难以理解的话,还不如直接上代码
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;
public class MyExample {
public static void main(String[] args) {
//通过键盘输入类名
String name;
Scanner in = new Scanner(System.in);
System.out.println("请输入类名的全限定名称:");
name = in.next();
//使用上面说的第二种forName的方式获取Class实例
//然后需要try catch一下捕获异常
try {
//获取输入类的Class实例
Class cl = Class.forName(name);
//获取输入类的父类的Class对象实例,如果是Object就不输出了,因为默认继承
Class supercl = cl.getSuperclass();
//Modifier是修饰符工具类,静态的,可以通过Class对象的getModifiers方法
//获取到修饰符对应的int数值,然后使用Modifier转换为字符串输出
String modifiers = Modifier.toString(cl.getModifiers());
if (modifiers.length() > 0){
System.out.print(modifiers + " ");
}
System.out.print("class " + name);
//这里注意下.class属性是可以直接用==进行比较的,
//所以判断supercl是不是Object类的Class对象可以直接和Object.class比较
if (supercl != null && supercl != Object.class){
System.out.print(" extends " + supercl.getName());
}
System.out.print("\n{\n");
//输出类中的所有构造器(构造函数)
printConstructors(cl);
System.out.println();
//输出类中的所有方法
printMethods(cl);
System.out.println();
//输出类中所有属性
printFields(cl);
System.out.println("}");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 打印类中所以的构造器
* @Param Class 对象
*
*/
public static void printConstructors(Class cl) {
//通过Class对象获取到类的所有构造器方法并放入数组
Constructor[] constructors = cl.getConstructors();
//这个增强for循环要是不太懂可以重新看下控制流程部分或者百度
for(Constructor constructor : constructors){
//这个不是获取Class对象的名字啊,这是Constructor类的方法获取构造器的名字
String name = constructor.getName();
System.out.print(" ");
//获取构造器的修饰符
String modifiers = Modifier.toString(constructor.getModifiers());
if (modifiers.length() > 0){
System.out.print(modifiers + " ");
}
//修饰符后面跟着构造器名字
System.out.print(name + "(");
//getParameterTypes方法返回一个描述参数类型的Class对象数组
//这里参数如果是类也是返回全限定类名,如果是类型那就是类型名,比如int
Class[] paramTypes = constructor.getParameterTypes();
for(int j = 0 ; j < paramTypes.length ; j++){
if (j > 0){
System.out.print(", ");
}
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
* 打印类中的所有方法
* @Param Class 对象
*
*/
public static void printMethods(Class cl){
//首先通过getMethods方法取得类中所有的方法
Method[] methods = cl.getMethods();
for(Method method : methods){
//获取方法名字
String name = method.getName();
//获取方法返回值类型
Class retType = method.getReturnType();
System.out.print(" ");
//获取方法的修饰符
String modifiers = Modifier.toString(method.getModifiers());
if (modifiers.length() > 0){
System.out.print(modifiers + " ");
}
System.out.print(retType.getName() + " " + name + "(");
//获取方法的参数,和上面的一样
Class[] paramTypes = method.getParameterTypes();
for(int j = 0 ; j < paramTypes.length ; j++){
if (j > 0){
System.out.print(", ");
}
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
/**
*打印类中所有域
* @Param Class 对象
*
*/
public static void printFields(Class cl){
//取出类中所有的域(属性),
//getDeclaredFields为什么不是getFields呢,这个下面讲
Field[] fields = cl.getDeclaredFields();
//这下面都是一样的,没什么好说
for (Field field : fields){
Class type = field.getType();
String name = field.getName();
System.out.print(" ");
String modifiers = Modifier.toString(field.getModifiers());
if (modifiers.length() > 0){
System.out.print(modifiers + " ");
}
System.out.println(type.getName() + " " + name + ";");
}
}
}
这个例子就是我们键盘输入一个类的全限定类名,可以打印出这个类的构造器信息,方法信息,属性信息。
需要注意的地方:
1.关于Modifier类静态方法获取修饰符的使用,获取到的修饰符是int类型的,然后Modifier.toString方法被重写了,里面运用位运算拼接了一个StringBuilder字符串,这就是public这种修饰符
2.getFields getMethods getConstructors方法分别返回的是类的public域,公有方法和构造器数组(public),其中包括父类的公有成员;
getDeclareFields getDeclareMethods getDeclareConstructors 方法返回的是类的全部域(包括private protected),全部方法和构造器数组,但是不包括父类的成员
3.还有例子中没体现的,如果某个方法需要抛出异常,有没有办法获取呢 ,可以用getExceptionTypes方法,返回类型是Class数组,Constructor和Method对象可以使用这个方法
4.Method对象调用getDeclaringClass可以根据方法对象返回一个方法对应的类对象,这个也是比较常用的方法。
小结:首先取得类的Class对象,然后用get*方法获取Field Method Constructor对象,然后获取具体信息。
三、在运行时使用反射分析对象
我们前面已经能够通过反射机制获取到类的域,方法,构造器,那从域的角度来说,是不是可以根据域名或者说属性名来获取、更改属性值呢。
首先我们要先有一个Field对象f,然后有一个Class对象c,属性值 = f.get(c); 这部分是反过来的,正常获取应该是 属性值 = c.getF();
同样的,如果要设置某个对象特定域值的属性,f.set(c,value);value就是你想给属性赋的值。
这个格式对于初学者可能不太容易理解,可以先忽略往下看。
需要注意的是:get方法是没法直接访问私有域的,会抛出IllegalAccessException没有访问权限的异常,但是我们可以调用setAccessible方法来跳过权限检查
然后我们在写一个例子:
package com.boe;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
public class ObjectAnalyzer {
//这个数组对象是为了防止无限递归,所以保存已经执行过的对象
private ArrayList<Object> visited = new ArrayList<>();
/**
* 将对象的所以域值转换成字符串表示
* @param obj 一个对象
* @return 对象的域值拼接成的字符串
*/
public String toString(Object obj) {
//如果传入的对象是null,那么返回一个字符串null
if (obj == null)
return "null";
//比较此对象是否被递归执行过
if (visited.contains(obj))
return "...";
visited.add(obj);
//获取传入参数的Class对象
Class cl = obj.getClass();
if (cl == String.class)
return (String)obj;
//isArray确定此类对象是否表示数组类
if (cl.isArray()){
//getComponentType方法是取得数组类的Class对象,这里是直接因为字符串拼接转换成字符串格式了,正常应该是Class对象
String r = cl.getComponentType() + "[]{";
//这里的Array是反射包中的类
for(int i = 0; i < Array.getLength(obj); i++){
if (i > 0){
r += ",";
}
//获取数组对象obj中下标为i的对象的值
Object val = Array.get(obj,i);
//判断Class是否为原始类型(boolean、char、byte、short、int、long、float、double)
//Class要是原始类型,val肯定是可以直接加到字符串,如果是引用类型,那就在用this.toString方法
//调用递归进行转换
if (cl.getComponentType().isPrimitive()){
r += val;
}else {
r += toString(val);
}
}
return r + "}";
}
String r = cl.getName();
do{
r += "[";
Field[] fields = cl.getDeclaredFields();
//设置fields中所有域都可以正常访问
AccessibleObject.setAccessible(fields,true);
for(Field field : fields){
//这里静态修饰符修饰的域不取值,因为和对象并没有关系
if (!Modifier.isStatic(field.getModifiers())){
if (!r.endsWith("[")){
r += ",";
}
r += field.getName() + "=";
try {
Class t = field.getType();
//获取obj对象中field对象对应的域的值
Object val = field.get(obj);
//判断Class是否为原始类型(boolean、char、byte、short、int、long、float、double)
if (t.isPrimitive()){
r += val;
}else {
//还是递归调用
r += toString(val);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
r += "]";
cl = cl.getSuperclass();
//如果这个类没有父类了,说明是对于传入的形参obj来说,它的域值都执行完毕转换成字符串输出了,就退出循环
}while (cl != null);
return r;
}
}
测试类:
import java.util.ArrayList;
public class Test001 {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 1 ; i < 7 ; i ++ ){
arrayList.add(i * i);
}
System.out.println(new ObjectAnalyzer().toString(arrayList));
}
}
其实这个例子是挺复杂的,有递归调用,还有些编程技巧,对新手不太友好,而且例子写的还是有一些bug(笑),我也是基本抄书。
要掌握的就是如何根据域对象获取到原始对象中域值和更改原始对象中域值,记住get和set,然后把平时编程的思维倒过来
再说一句,有关setAccessible()方法
void setAccessible(boolean flag) | 为反射对象设置可访问标志,flag为true表示屏蔽Java语言的访问检查,使对象的私有属性也可以被查询和设置 |
boolean isAccessible() | 返回反射对象的可访问标志的值 |
static void setAccessible(AccessibleObject[] array, boolean flag) | 设置对象数组可访问标志的快捷方法 |
四、使用反射编写泛型数组代码
我们应该知道不管是原始类型数组还是引用类型数组,都只能是相同的类型,那如果需要写一个通用的CopyOf来拷贝数组能不能实现呢 ,
首先拷贝数组需要有原数组,包括长度,元素内容,类型;长度和元素内容都是很容易获取到也很容易copy的内容,但是由于想实现通用,那么类型的考虑就变得很重要
Object类作为所有类的父类能不能实现呢,简单写一下
public static Object[] badCopyOf(Object[] a , int newLength){
Object[] newArray = new Object[newLength];
System.arraycopy(a,0,newArray,0,Math.min(a.length,newLength));
return newArray;
}
有两个问题,第一个传输进来的数组不能是基本类型,比如int[]是不能转换为Object[]的,但是可以转换为Object,第二个问题,即使拷贝成功,拷贝后的数组是Object[]类型的,无法转换成员类型。
然后我们尝试用反射实现一下。
public static Object goodCopyOf(Object a ,int newLength){
Class cl = a.getClass();
//判断是否是数组类型
if (cl.isArray()){
//获取数组中元素的类型
Class comp = cl.getComponentType();
int length = Array.getLength(a);
Object newArray = Array.newInstance(comp,newLength);
System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
return newArray;
}
return null;
}
- 首先获得a数组的类对象
- 确认它是一个数组
- 使用Class类的getComponentType方法确定数组对应的类型
这个方法就可以CopyOf任意类型的数组了。
值得注意的是:重点在于newInstance这个方法,Array类中的newInstance可以实例化数组对象,Class类中的newInstance可以实例化对象,我们使用的Spring框架有用到这个方法来实例化对象,反射机制在框架中使用的非常多。
五、调用任意方法
在反射机制中是允许调用任意方法的,原理和Field对象的使用有些相似。
首先,获取到对象的Class类实例,然后调用getDeclareMethods或者是getMethod方法获取Method对象,找到我们想调用的方法
然后,使用Method类中的invoke方法执行,和Field中的get有点像
Object invoke(Object obj, Object... args)
第一个参数是隐式参数,也就是我们用于获取Class类的对象,对于静态方法,可以将第一个参数设置为null,后面是方法的显式参数,如果返回类型是基本类型,invoke方法会返回其包装器类型。
因为invoke的参数类型和返回值都是Object类型的,所以实际应用中会涉及到类型转换,这是容易出错且不易排查的地方,使用反射机制调用方法也要比直接调用更慢,如果没什么必要,就别使用反射了,正常情况下开发很少用到,如果要了解框架实现,反射必须要学好。
这篇文章是我作为初学者的一点笔记,希望各位大佬在观看后能批评指正。