Java反射难理解?看我这篇!

本文详细讲解了Java反射的概念、Class对象的结构、反射过程,以及其在对象创建、注解、动态代理和解耦中的实际应用。通过实例演示了如何通过反射获取属性、方法和创建对象,揭示了反射在高级编程中的重要作用。

什么是反射

Java程序员在谈到找男/女朋友的时候说的最多的一句话:找对象?new一个
虽是一句玩笑话,但也透射出平常编码的一个习惯:new语句去创建对象,因为这个实在太简单好用,直观且方便,但new语句不能真正体现Java创建对象的过程,反射才是。
不光在创建对象上,Java的注解、动态代理包括spring框架里的aop/ioc都是基于反射或跟反射有很大关联,所以不理解反射后续的进阶学习寸步难行,而理解反射之前,必须要知道class对象的概念,如果连这个概念都不太清楚那么理解反射的很吃力(俄罗斯套娃,一娃套一娃),可以先看下之前的一篇文章解析Java创建一个对象的过程,这里我就一笔带过了:

  • 先给对象分配内存(包括本类和父类的所有实例变量,不包括上面的静态变量),并设置默认值,如果有引用对象那么会在栈内存中申请一个空间用来指向的实际对象。
  • 执行初始化代码实例化,先初始化父类再初始化子类,赋予给定值(尊重长辈是java的传统美德)
  • 对象实例化完毕后如果存在引用对象的话还需要**把第一步的栈对象指向到堆内存中的实际对象,**这样一个真正可用的对象才被创建出来。
    image.png
    从上面的流程图可以看到,Class对象是JVM中生成对象的依据来源,而反射的本质其实就是用java.lang.reflect 类库API操作Class对象的结果!

Class对象

类加载器 ClassLoader 加载class文件时,会把类里的一些数值常量、方法、类信息等加载到内存中,称之为类的元数据,最终目的是为了生成一个Class对象用来描述类,这个对象会被保存在.class文件里,可能有新手看到这里会比较懵逼,class也有对象?当然了,Class是个实实在在的类(用来描述类的类,比较拗口),有构造方法( private意味着可以生成对象,但不能手动生成,由JVM自动创建Class对象
image.png
类加载器会给每个java文件都创建一个Class对象,用来描述类,画个图:
image.png

//以下操作只能由jvm完成,我们手动做不了,在Class源码的137行可以看到构造器是private的
Class cls1 = new Class(Dog.class.getClassLoader());
Class cls2 = new Class(Cat.class.getClassLoader());
Class cls3 = new Class(People.class.getClassLoader());

上面说了,Class类是用来描述像People类的类,那么它里面肯定包含了所有能够描述该class的所有共通属性,比如类名、方法、接口等,我们先到Class类源码中瞄一眼,会发现在2464行处有一个内部私有类 ReflectionData (反射数据),里面有 Field 字段类实例、 Method 方法类实例、 Constructor 构造器类实例以及接口类实例。

// reflection data that might get invalidated when JVM TI RedefineClasses() is called
private static class ReflectionData<T> {
    //declared关键字代表获取所有
    volatile Field[] declaredFields;				//字段类实例
    volatile Field[] publicFields;
    volatile Method[] declaredMethods;				//方法类实例
    volatile Method[] publicMethods;
    volatile Constructor<T>[] declaredConstructors;	//构造器类实例
    volatile Constructor<T>[] publicConstructors;
    // Intermediate results for getFields and getMethods
    volatile Field[] declaredPublicFields;
    volatile Method[] declaredPublicMethods;
    volatile Class<?>[] interfaces;

    // Value of classRedefinedCount when we created this ReflectionData instance
    final int redefinedCount;

    ReflectionData(int redefinedCount) {
        this.redefinedCount = redefinedCount;
    }
}

java.lang.reflect操作类之Field

其实除了上述几个实例之外,还有一些反射数据在Class类里面,但起到的作用都是一样的:描述目标类的所有信息,比如拿Field 类来说,抽一部分源码解读(我加了点中文注释解析):

private Class<?>            clazz;							//类对象
private int                 slot;							//变量占据大小
// This is guaranteed to be interned by the VM in the 1.4
// reflection implementation
private String              name;							//变量名称实例
private Class<?>            type;							//变量类型实例
private int                 modifiers;						//变量修饰符实例
// Generics and annotations support
private transient String    signature;						//一些泛型和注解的支持
// generic info repository; lazily initialized
private transient FieldRepository genericInfo;
private byte[]              annotations;					//变量注释实例

那么实际一个变量和class对象中的字段类Field映射关系应该是这样的:
image.png

java.lang.reflect操作类之Method

又比如拿Method类来说,一样抽取它一部分源码看(加了点中文注释解析):

private Class<?>            clazz;					//类对象
private int                 slot;
// This is guaranteed to be interned by the VM in the 1.4
// reflection implementation
private String              name;					//方法名实例
private Class<?>            returnType;				//方法出参类型实例
private Class<?>[]          parameterTypes;			//方法入参类型实例
private Class<?>[]          exceptionTypes;			//方法异常类型实例
private int                 modifiers;				//方法修饰词类型实例
// Generics and annotations support
private transient String              signature;	//一些泛型和注解的支持
// generic info repository; lazily initialized
private transient MethodRepository genericInfo;
private byte[]              annotations;			//方法注释实例
private byte[]              parameterAnnotations;	//方法参数注释实例
private byte[]              annotationDefault;		//
private volatile MethodAccessor methodAccessor;

那么实际一个方法和class对象中的Method方法类映射关系应该是这样的:
image.png
所以说Class类实例中包含了描述目标类的所有信息,这些信息为后面反射调用实例变量、方法、构造函数提供了依据
image.png

反射过程以及如何反射

好了,反射依据有了,那么反射的过程是什么样子的?有些人觉得可能是这样的:
image.png
但实际上是这样的,反射获取类属性的过程并不需要对象去参与,但是调用方法和变量需要实例对象的参与:
image.png
如何不通过创建一个对象去反射调用目标类的属性和方法,比如有下面这么一个类,我们该怎样通过反射获取该类的信息?

/**
 * @author :炜哥
 * @date :创建于 2021/4/27 19:08
 * @description:
 * @modified By:
 * @version: 1.0
 */
public class Animal {

    //私有成员变量
    private String name = "动物";

    //公有成员变量
    public int age = 7;

    //无参构造函数
    private Animal() {
        System.out.println("调用公有无参构造方法");
    }

    //公有有参构造函数
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("调用公有构造方法name:" + this.name + " age:" + this.age);
    }

    //私有有参构造函数
    private Animal(String name) {
        System.out.println("调用私有构造方法name:" + this.name);
    }

    //公有成员方法
    public void showName(String name) {
        System.out.println("调用公有成员方法,输入名字:" + name + " 内部name:" + this.name);
    }

    //私有成员方法
    private void showAge(int age) {
        System.out.println("调用私有成员方法,输入年龄:" + age);
    }

    //main方法
    public static void main(String[] args) {
        System.out.println("调用main方法");
    }
}

JDK提供了大量操作api,大概画了张图罗列了下(方法很多,但不用慌,实际在使用过程中只要进入源码中能看懂就行了,因为这些方法平常也没多大机会去写,反射一般都集成在框架中,这些知识点敲一遍就行,心中有个数):
image.png
端上新鲜出炉的代码:

反射和调用构造函数

Class clazz = Class.forName("com.service.reflection.Animal");
//获取公有有参构造类
Constructor public_params_constructor = clazz.getConstructor(String.class, Integer.class);
System.out.println("反射获取公有有参构造类:" + public_params_constructor);

//获取私有无参构造类
Constructor private_params_construtor = clazz.getDeclaredConstructor(null);
private_params_construtor.setAccessible(true);//设置允许访问,忽略修饰符
System.out.println("反射获取私有无参构造类:" + private_params_construtor);

//两者都可以通过newInstance来创建对象
Object obj = private_params_construtor.newInstance();//方式一
obj = public_params_constructor.newInstance("狗子", 10);//方式二

Animal animal = (Animal) obj;
System.out.println("年龄:" + animal.age);

执行输出:

反射获取公有有参构造类:public com.service.reflection.Animal(java.lang.String,java.lang.Integer)
反射获取私有无参构造类:private com.service.reflection.Animal()
调用公有无参构造方法
调用公有构造方法name:狗子 age:10
年龄:10

上面可能有人会问, Animal 的公共有参方法明明是 public Animal(String name, int age) ,为什么 Integer.class 能够对应 int 这个基本数据类型呢?这是java1.5以后提供的自动拆箱和自动装箱干的事,即会把包装类自动拆箱为对应的基本数据类型。

反射和调用成员变量

Class clazz = Class.forName("com.service.reflection.Animal");
//获取私有字段
Field private_field = clazz.getDeclaredField("name");
System.out.println("反射获取私有成员变量:" + private_field);
//获取公共字段
Field public_field = clazz.getField("age");
System.out.println("反射获取公共成员变量:" + public_field);

//调用字段
Object o = clazz.getConstructor(String.class, Integer.class).newInstance("狗子", 10);
public_field.set(o, 5);
private_field.setAccessible(true);//暴力反射,解除私有限定
private_field.set(o, "小白");

Animal animal = (Animal) o;
System.out.println("狗的年龄:" + animal.age);

执行输出:

反射获取私有成员变量:private java.lang.String com.service.reflection.Animal.name
反射获取公共成员变量:public int com.service.reflection.Animal.age
调用公有构造方法name:狗子 age:10
狗的年龄:5

反射和调用成员方法

Class clazz = Class.forName("com.service.reflection.Animal");
//获取公有方法
Method public_method = clazz.getMethod("showName", String.class);
System.out.println("公共方法:" + public_method);

Method private_method = clazz.getDeclaredMethod("showAge", int.class);
System.out.println("私有方法:" + private_method);

//调用字段
Object o = clazz.getConstructor(String.class, Integer.class).newInstance("狗子", 10);
public_method.invoke(o,"小黑");
private_method.setAccessible(true);//暴力反射
private_method.invoke(o,4);

Animal animal = (Animal) o;
System.out.println("狗的年龄:" + animal.age);

执行输出:

公共方法:public void com.service.reflection.Animal.showName(java.lang.String)
私有方法:private void com.service.reflection.Animal.showAge(int)
调用公有构造方法name:狗子 age:10
调用公有成员方法,输入名字:小黑 内部name:狗子
调用私有成员方法,输入年龄:4
狗的年龄:10

相信执行这些代码的逻辑应该都能理解,但是上面有个 int.class 可能有疑惑,它跟 Integer.class 有什么区别

  • int.class等价于Integer.Type,后者是Integer类中的一个静态常量,所以肯定不等价与Integer.class

image.png

  • int.class顶多算一个class对象引用,因为int不是一个类,而Integer是一个实实在在的类(包装类)

反射有啥用?

叽叽歪歪说了半天,这反射有啥用,实际码代码的时候也没见能让我写多少啊?
记住一句话,Java的一些老生常谈、强调非常重要的、实际却很少看到的理念一般都被集成在了框架中,目的是为了屏蔽一些难以理解的细节,让开发者专注于业务逻辑的实现,况且反射效率低,正经人谁在写业务逻辑时创建对象用反射啊(不过这个效率低是要打上问号的,平常编码中怎么可能感觉的出来),进入正题:

反射过程中不可缺少的一步是创建Class对象,创建Class对象其中有一种方式是只需要知道目标类的全限定名(包名+类名)就行了Class.forName("全限定名") ,这样就带来一个好处,即不存在该类也能通过编译期,因为全限定名对于代码来讲只是一个字符串,而字符串可以作为变量去定义,这就避免了把代码写死后续遇到问题需要大面积修改的尴尬,我又要开始举例了,而且是亲身经历的一个例子:
有次因为要对接用户的实名认证,一开始用的是阿里云实名认证接口,但属实效果太差,识别精度低,各种出错,后面换成了百度AI,效果还不错,精度提高了不少而且每天免费额度有500次,但后面需求改了,需要返回一系列的身份证反光、边角缺失等一系列的提示,所以又找了腾讯AI… …所以,如果是你,会怎么设计这套逻辑?我想,一半的人会这样做:

//发现不好用,注释
/*AliIdCardUtil a = new AliIdCardUtil(身份证图片信息);
return 识别信息 = a.identify();*/

//发现不能用,注释
/*BaiduIdCardUtil b = new BaiduIdCardUtil(身份证图片信息);
return 识别信息 = b.identify();*/

//最终用了这个
TencentIdCardUtil t = new TencentIdCardUtil(身份证图片信息)return 识别信息 = t.identify();

不要笑,在任务重工期紧的情况下,在你的老板和上级只要求能实现功能的时候这样做又有何不可?只是它违背了类设计原则中的开闭原则,即对扩展开放,对修改关闭,如果代码扩展需要用注释来解决,那么设计这门语言的人也太蠢了(无意冒犯Gosling祖师爷),所以针对上述实际情况,我们结合反射可以这样做,先定义一个身份证识别接口interface(不要在意语法,就是讲个大概的思路):

interface IdentifyIdCardService {
    getIdCardInfo(身份证图片信息);
}

然后定义多个识别的实现接口:

//阿里云身份证识别信息
public class IdentifyByAliServiceImpl implments IdentifyIdCardService{
    getIdCardInfo(身份证图片信息){
        
        ...识别方法...
        
        return 身份证识别信息;
    }
}

//百度AI身份证识别信息
public class IdentifyByBaiduServiceImpl implments IdentifyIdCardService{
    getIdCardInfo(身份证图片信息){
        
        ...识别方法...
        
        return 身份证识别信息;
    }
}

//腾讯AI身份证识别信息
public class IdentifyByTencentServiceImpl implments IdentifyIdCardService{
    getIdCardInfo(身份证图片信息){
        
        ...识别方法...
        
        return 身份证识别信息;
    }
}

然后正常情况(不用反射)下我们大概会这样做:

IdentifyIdCardService i = new IdentifyByAliServiceImpl();
return i.getIdCardInfo(身份证图片信息);//拿到身份证识别信息

接着我们就可以根据上面的语句按照反射原理稍微改进一下:

String serviceImplUrl = 从数据库中读取得到的上述三个实现类的路径+包名地址;
IdentifyIdCardService i = Class.forName(serviceImplUrl).newInstance();
return i.getIdCardInfo(身份证图片);//拿到身份证识别信息

上述代码实现了client端代码跟service实现类代码的解耦,因为client不需要关心service到底是被哪个实现类所实现,只需跟接口进行交互,还体现了java多态以及六大设计原则中的依赖倒置依赖于抽象编程,而不是实现),如果因为实际情况需要切换api服务商也只需修改 serviceImplUrl 的值就能实现(比如修改数据库),就算需要再增加一个识别AI平台也只要增加实现 IdentifyIdCardService 接口的实现类,原先的3个实现类根本不需要动。

上述例子体现了反射可以帮助程序降耦甚至解耦的一个作用(实际可能还会有更好的解决办法),除此之外,反射还有很多其他用途:

  • 实现自定义注解,了解注解的应该知道,Java的自定义注解其实就是通过反射获取FieldMethodConstructor 等类对象中的注解Annotation实例,通过解析注解实例并执行自定义注解方法,从而体现自定义注解的作用(注解的文章也快写完了,过段时间马上出炉)
  • 实现动态代理,动态代理中非常重要的一步就是通过 Proxy 类复制一个目标类的克隆类Class对象(比目标类多一个构造器),通过这个克隆类Class对象反射构造器来获得代理对象,从而顺理成章地实现代码增强(看不懂没关系,关于代理的文章已经写一半了,看完之后你肯定能懂,嘿嘿嘿),还有代理对象和增强方法的解耦也是通过反射实现(用到了 InvocationHandler 类)。
  • 可以访问一些私有变量和属性:上面的例子你也看到了,我们可以通过 getDeclaredMethodgetDeclaredField 获得私有方法和私有属性,看到这个有人可能会问,那我在类中设置 private 还有什么意义吗?有,我理解的private更多体现在规范上面,就好比,你按照我定的规则去开发,会得到我设定的期望结果;如果不遵守,出现什么后果我可不负责,总之就是为了保障开发者的设计逻辑安全,而不是什么数据安全什么的。

OVER

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值