一文带你快速了解Java中的反射机制

反射初步学习

1、反射基础概念

1.1 什么是反射?

反射(Reflection)是一种计算机的处理机制。Java中,当程序在运行时,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键。

 总结而言:反射就是“Java中对于任何一个类,在运行时都能直接得到它的全部成分“的这个机制。

反射的原理:Java中所有的类在被javac编译后都会经过类加载器(classLoader)加载到Class类中(此时是以.class文件的形式存在),该类中有成员变量(field)、成员方法(methods)、构造方法(constructor)等类的成分。而.class文件是在Java运行阶段执行的,所以我们只需要获取到Class类的类对象,就能够在Java程序运行时获取我们想要的类的全部信息,从而实现反射机制。过程如图所示:

image-20220620084700714

拓展:类加载

当程序要使用某一个类时,这个类还没有加载到内存中,(如果不出意外)则JVM会连续自动完成 类的加载类的链接类的初始化三个步骤对类进行初始化,通常将这三步合称为:类加载(或类初始化

  • 类的加载

    1. JVM将class文件读入内存,并为之创建一个在java.lang.Class对象
    2. 任何类被使用时,都会创建一个java.lang.Class类对象
  • 类的链接

    1. 验证阶段:检验被加载的类是否具有正确的内部结构,同时是否和其他类协调一致
    2. 准备阶段:负责为类中变量分配内存,并设置初始值
    3. 解析阶段:将类的二进制数据中的符号引用替换成直接引用
  • 类的初始化

    1. 如果类未进行加载和链接,则先进性加载和链接(只有完成前面两部才能进行初始化
    2. 如果类的直接父类未被初始化,先对直接父类进行初始化(所有子类的构造方法都默认第一行有super()方法的缘故)
    3. 如果类有初始化语句,则先执行初始化语句
  • 类的初始化时机

    1. 创建类的实例化对象时
    2. 初始化子类时
    3. 调用了类的方法时
    4. 使用反射方式强制创建某个类的java.lang.Class对象
  • JVM类加载器机制

    1. 全盘负责:当一个类被加载时,该类所依赖和引用的其他类都会由该类的加载器加载(除非强制要其他类加载器加载)
    2. 父类委托:当一个类被加载时,先让父类的加载器加载该类,只有当父类加载器加载失败时才让该类的类加载器加载
    3. 缓存机制:保证所有加载过的类都会被存入缓存,当程序需要使用某个Class对象时,都会先到缓存中搜索,缓存中没搜索到,JVM就会读取该类的二进制数据,将其转成Class是对象并存到缓存中

常见的类加载器:Bootsrtrap class loader(JVM内置类加载器)、Platform class loader(平台类加载器)、System class loader

  • static ClassLoader getSystemClassLoader()返回当前类委派的系统类加载器
  • ClassLoader getParent()返回父类加载器
1.2 反射的优点和缺点
  • 优点
  1. 提高了程序的灵活性和扩展性。反射可以让程序员在程序运行阶段操作对象
  2. 降低耦合性,提高自适应能力

……

  • 缺点
  1. 降低了程序的效率。反射是一种解释操作,使用反射机制的代码要比直接代码效率低
  2. 提高了程序的复杂度,提高程序的维护难度。反射会模糊程序内部逻辑
  3. 不够安全。使用反射通常需要程序的运行没有安全方面的限制,如果一个程序对安全性提出要求,则最好不要使用反射
  4. 不符合面对对象的设计。反射违背了面对对象的基本特征封装,它可以无视权限修饰符(private……)【这点也不能完全说是缺点】

……


2、反射相关操作

  • 在暴力反射setAccessible(true)面前所有的权限关键字(private、public……)都不起作用,真🐮 。

  • 跟反射相关的操作都是需要先获取类对象的,因为只有操作.class文件才能在Java程序运行时进行动态操作

2.1 获取类对象
方法示例(获取Person类对应的地址赋值给c这个类对象)
Class.forName(“全类名”)Class c = Class.forName("com.hhxy.reflect.Person");
类名.classClass c = Person.class;
对象名.getClass()Class c = p.getClass();

备注:

1)第一种方法多用于配置文件,将类名定义在配置文件中。读取文件,加载类

2)第二种方法多用于参数的传递

3)第三种方法多用于多用于对象的获取字节码的方式

4)同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。eg:

Class c1 =  Class.forName("com.hhxy.reflect.Person");
Class c2 = Person.class;
Person p = new Person();
Class c3 = p.getClass();
System.out.println(c1==c2 && c1==c3 && c2==c3);//输出true
2.2 获取类对象中的成员变量

Field英文中的意思为田野的意识,小写在Java中是成员变量的意思,同时它(大写)也是一个类名,表示成员类,位于java.lang.reflect包下

先要获取类对象,通过类对象来操作成员变量:

Class sClass = Student.class;//获取Student类的类对象
方法作用示例
getFields()获取类对象中public修饰的所有成员变量Field[] fields = sClass.getFields();
getFields(String st)获取类对象中public修饰的指定的的成员变量Field field = sClass.getField("a");
getDeclaredFields()获取类对象中所有的成员变量Field[] fields = sClass.getDeclaredFields();
Field getDeclaredField(String st)获取类对象中指定的成员变量Field field ==sClass.getDeclaredField("address");
  • sClass.getSimpleName() 获取类名
  • sClass.getName 获取全类名(包名+类名)

备注:

1)四种方法返回值都是Field对象,需要用Field对象来接

2)带有declared的是不受权限修饰符(public、private……)的影响

3)带s的返回值都是对象数组,需要用Field[]对象数组来接

4)当指定对象不存在、指定对象存在但是修饰符不匹配会报NoSuchFieldException(不存在该成员变量异常)

2.3 获取、设置类对象中成员变量的值

先需要获取类对象、实例化待操作的类、获取类对象中的成员变量:

Class sClass = Student.class;//将Student类所对应的地址赋值给sClass类对象
Student s = new Student();//示例化Student类
Field adr = sClass.getDeclaredField("address");//返回的是s.address地址,然后将地址赋值给adr
方法作用示例
get()获取成员变量的值Object value = adr.get(s);//获取s对象中address属性的值
set()修改成员变量的值adr.set(s,"广州");//将s对象中的属性address的值修改为"广州"
  • adr.getType(); 获取类对象中成员变量的数据类型

反射命名的由来(趣谈,别当真):

不使用反射:s.setAddress("广州");//对象去主动修改address属性的值

使用反射:adr.set(s,"广州");//address属性主动去要求对象修改自己的值(反过来了,最终效果是一样的)

备注:

get()方法在获取成员变量的值时,以及set()方法在修改成员变量时,如果成员变量由private修饰会报IllegalAccessException(非法访问异常),需要使用==暴力反射adr.setAccessible(true)==才能进行修改

2.4 获取类对象中构造器方法

先要获取类对象

Class sClass = Student.class;//获取Student类的类对象

image-20220620085324276

eg:

备注:我没有给Student类设置构造器,所以默认是有一个无参的构造器

package com.hhxy.reflect;
import java.lang.reflect.Constructor;
//获取类对象中构造器的四种方法:
public class ReflectDemo4 {
    public static void main(String[] args) {
        //Stept1:获取类对象
        Class sClass = Student.class;
        //Stept2:使用getConstructors()方法获取类对象中所有public修饰的构造器
        Constructor[] constructors1 = sClass.getConstructors();
        //Stept3:遍历类对象中的构造器对象
        for (Constructor constructor : constructors1) {
            System.out.println(constructor);//得到的是构造器的方法名:public com.hhxy.reflect.Student()
            System.out.println(constructor.getName());//得到构造器的名字:com.hhxy.reflect.Student
            System.out.println(constructor.getParameterCount());//获取构造器中的形参的个数:0
        }
    }
}
2.5 获取类对象中成员方法

先要获取类对象:

Class sClass = Student.class;//获取Student类的类对象
Method[] method = sClass.getDeclaredMethods();//获取成员变量中成员方法对象

image-20220620110201035

  • method.getName(); 获取成员方法名
  • method.getReturnType(); 获取成员方法的返回值的数据类型
  • method.getParameterCount; 获取成员方法中形参的个数

触发(执行)方法:invoke

image-20220620110215714

Class sClass = Student.class;//获取Student类的类对象
Method[] m = sClass.getDeclaredMethods("say",String.class);//获取成员变量中成员方法对象
Student s = new Student();
m.invoke(s,"张三");//输出:我是张三

3、 反射的的相关应用

  1. 绕过编译阶段强行为集合添加任何类型的数据

    不使用反射:
    ArrayList<Integer> list = new ArrayList<>();
    list.add(100);
    list.add("abc"); // 报错
    使用反射:
    Class listClass = list.getClass();//获取类Arrary类的类对象
    Method add = listClass.getDeclaredMethod("add",Object.class);//获取类对象中的成员方法对象
    add.invoke(list,"abc");//触发list对象中的add方法
    System.out.println(list);
    

    原因:泛型只是在编译阶段可以约束集合只能操作某种数据类型,在编译成Class文件进入运行阶段的时候,其真实类型都是ArrayList了,泛型相当于被擦除,目的是为了节约内存(不全是,也有一部分原因是因为为了向前兼容

    ArrayList<String> list1 = new ArrayList<>();
    ArrayList<Integer> list2 = new ArrayList<>();
    //ArrayList<>通过类加载器加载到.class文件中,都统一成ArrayList对象了
    System.out.println(list1.getClass());//class java.util.ArrayList
    System.out.println(list2.getClass());//class java.util.ArrayList
    System.out.println(list1.getClass()==list2.getClass());//true
    
  2. 实现动态操作。能够在程序运行阶段获取对象、调用方法、操作注解

  3. 动态链接。Idear这款软件是用Java写的,我们在使用对象名.然后弹出方法提示框就是利用了反射机制,将Class类中的方法对象与我们创建的对象进行了动态链接

  4. 制作框架。利用的原理和1是一样的,都是利用反射能绕开编译器的审查机制,同时反射是框架的灵魂

    案例:制作一个通用框架。使用反射机制实现只需要修改配置文件就能直接调用不同类的不同方法,告别调用不同类的方法还要去修改代码的麻烦操作。

    Person类

    package com.hhxy.reflect;
    
    public class Person {
        private String name;
        private int age;
        private char sex;
    
        public Person() {
        }
    
        public Person(String name, int age, char sex) {
            this.name = name;
            this.age = age;
            this.sex = sex;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public char getSex() {
            return sex;
        }
    
        public void setSex(char sex) {
            this.sex = sex;
        }
    
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", sex=" + sex +
                    '}';
        }
        public void eat(){
            System.out.println("eat...");
        }
        public void eat(String food){
            System.out.println("eat..."+food);
        }
    }
    

    Student类

    package com.hhxy.reflect;
    
    public class Student {
        public void sleep(){
            System.out.println("sleep...");
        }
    }
    

    pro.properties配置文件

    className=com.hhxy.reflect.Student
    methodName=sleep
    

    FramTest类

    package com.hhxy.reflect;
    
    import java.io.InputStream;
    import java.lang.reflect.Method;
    import java.util.Properties;
    
    /**
     * 框架类
     * 前提:不改变任何类的对象
     * 1.可以创建任意类的对象
     * 2.可以执行任意方法
     */
    public class FrameTest {
        /*不使用反射:需要修改大量代码(同时要重新测试)*/
    /*    public static void main(String[] args) {
            //当我想要调用Person类的方法时,需要创建Person类的对象
            Person p = new Person();
            p.eat();
            //当我想要调用Student类的方法时,需要创建Student类的对象
            Student s = new Student();
        }*/
        /*---------------------------------------------------------*/
        /*使用反射:只需要修改配置文件*/
        public static void main(String[] args) throws Exception {
            //1.加载配置文件
            //1.1 创建Properties对象
            Properties pro = new Properties();
            //1.2获取Framework类的类加载器
            ClassLoader classLoader = FrameTest.class.getClassLoader();
            //1.3获取配置文件中对应的字节流
            InputStream is = classLoader.getResourceAsStream("pro.properties");
            //1.4将字节流传进当前类的pro集合
            pro.load(is);
    
            //2.获取pro集合中的数据
            //2.1获取pro集合中className对应的value值,返回全类名
            String className = pro.getProperty("className");
            //2.2获取pro集合中methodName对应的value值,返回方法名
            String methodName = pro.getProperty("methodName");
    
            //3.获取类对象(需要将该类加载到内存,此时该类是 字节码文件的形式)
            Class cls= Class.forName(className);
    
            //4.创建加载到内存中字节码的对象
            Object obj = cls.newInstance();
    
            //5.获取方法对象
            Method method = cls.getMethod(methodName);
    
            //6.执行方法
            method.invoke(obj);
        }
    }
    
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知识汲取者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值