Java-反射机制

1.1 框架的概念

​ 半成品软件。可以在框架的基础上进行软件开发,简化编码。学习框架并不需要了解反射,但是要是想自己写一个框架,那么就要对反射机制有很深入的了解。

1.2 反射

​ 反射机制:将类的各个组成部分封装为其他对象,这就是反射机制。

反射的好处:

  1. 可以在程序运行过程中,操作这些对象。

  2. 可以解耦,提高程序的可扩展性。

补充:Java代码在计算机中经历的三个阶段:

U1vYJf.png
  1. Source源代码阶段:.java被编译成.class字节码文件。

  2. Class类对象阶段:.class字节码文件被类加载器加载进内存,并将其封装成Class对象(用于在内存中描述字节码文件),

    Class对象将原字节码文件中的成员变量抽取出来封装成数组Field[],

    将原字节码文件中的构造函数抽取出来封装成数组Construction[]

    在将成员方法封装成Method[]。当然Class类内不止这三个,还封装了很多,我们常用的就这三个。

  3. RunTime运行时阶段:创建对象的过程new。

2.1 获取Class对象的方式

获取Class对象的三种方式对应着java代码在计算机中的三个阶段

  1. Source源代码阶段

    Class.forName("全类名"):将字节码文件加载进内存,返回Class对象

    • 多用于配置文件,将类名定义在配置文件中。读取文件,加载类。
  2. Class类对象阶段

    类名.class:通过类名的属性class获取

    • 多用于参数的传递
  3. Runtime运行时阶段

    对象.getClass():getClass()方法是定义在Objec类中的方法

    • 多用于对象的获取字节码的方式

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

举例

public void reflect1() throws ClassNotFoundException {     
    //方式一:Class.forName("全类名");     
    Class cls1 =    Class.forName("com.test.domain.Person");   //Person自定义实体类     
    System.out.println("cls1 = " + cls1);      //方式二:类名.class  
    Class cls2 = Person.class;     
    System.out.println("cls2 = " + cls2);      //方式三:对象.getClass();
    Person person = new Person();             
    Class cls3 = person.getClass();     
    System.out.println("cls3 = " + cls3);      // == 比较三个对象
    System.out.println("cls1 == cls2 : " + (cls1 == cls2));    //true
    System.out.println("cls1 == cls3 : " + (cls1 == cls3));    //true

3.1 Class对象功能

详见JDK文档 JDK doc-zh

3.1.1 成员变量Field

Field[] getFields():获取所有public修饰的成员变量

Field getField(String name) 获取指定名称的 public修饰的成员变量

Field[] getDeclaredFields()获取所有的成员变量,不考虑修饰符

Field getDeclaredField(String name) 获取指定名称的所有的成员变量

测试类:

class Person {      
    public String a;            
    protected String b;       
    String c;            
    private String d;
}

Field的有关操作

  1. 设置值 void set(Object obj, Object value)

  2. 获取值 get(Object obj)

  3. 忽略访问权限修饰符的安全检查 setAccessible(true):暴力反射

测试:getFields和getField(String name)

public void reflect2() throws Exception {     
    //0、获取Person的Class对象     
    Class personClass = Person.class;      
    //1、Field[] getFields()获取所有public修饰的成员变量     
    Field[] fields = personClass.getFields();     
    for(Field field : fields){         
        System.out.println(field);     
    }
    System.out.println("=============================");     
    //2.Field getField(String name)     
    Field a = personClass.getField("a");      
    //获取成员变量a 的值 [也只能获取公有的,获取私有的或者不存在的字符会抛出异常]
    Person p = new Person();     
    Object value = a.get(p);     
    System.out.println("value = " + value); // value == null     
    //设置属性a的值     
    a.set(p,"张三");     
    System.out.println(p); // Person(name=null,a="张三", ...)
}

测试 getDeclaredFields和getDeclaredField(String name)方法

对于私有变量虽然能会获取到,但不能直接set和get ,如果需要,加上暴力破解

public void reflect3() throws Exception {     
    Class personClass = Person.class;      //Field[] getDeclaredFields():获取所有的成员变量,不考虑修饰符     
    Field[] declaredFields = personClass.getDeclaredFields();
    for(Field filed : declaredFields){
        System.out.println(filed);     
    }
    System.out.println("===================================");
    //Field getDeclaredField(String name)     
    Field d = personClass.getDeclaredField("d");
    //private String d;
    Person p = new Person();
    Object value1 = d.get(p);    //会抛出异常
    System.out.println("value1 = " + value1);    //对于私有变量虽然能会获取到,但不能直接set和get      
    //忽略访问权限修饰符的安全检查
    d.setAccessible(true);//暴力反射
    Object value2 = d.get(p);
    System.out.println("value2 = " + value2); }

3.1.2 获取构造方法Constructor

具体描述与命名方式与获取成员变量类似

Constructor<T> getConstructor(Class<?>... parameterTypes)

Constructor<?>[] getDeclaredConstructors()

Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)

获取的主要目的:创建对象

注意:如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法

此方法在JDK1.9后弃用

测试类:

public class Person {      
    private String name;     
    private Integer age;      
    //无参构造函数     
    public Person() {}          
    //单个参数的构造函数,且为私有构造方法
    private Person(String name){}      
    //有参构造函数
    public Person(String name, Integer age){
        this.name = name;
        this.age = age;     
        }
}

测试:

/**
 * 2. 获取构造方法们
 *    Constructor<?>[] getConstructors()
 *    Constructor<T> getConstructor(类<?>... parameterTypes)
 */
@Test
public void reflect4() throws Exception {
    Class personClass = Person.class;

    //Constructor<?>[] getConstructors()
    Constructor[] constructors = personClass.getConstructors();
    for(Constructor constructor : constructors){   //Constructor 对象reflect包下的 import java.lang.reflect.Constructor;
        System.out.println(constructor);
    }

    System.out.println("==========================================");

    //获取无参构造函数   注意:Person类中必须要有无参的构造函数,不然抛出异常
    Constructor constructor1 = personClass.getConstructor();
    System.out.println("constructor1 = " + constructor1);
    //获取到构造函数后可以用于创建对象
    Object person1 = constructor1.newInstance();//Constructor类内提供了初始化方法newInstance();方法
    System.out.println("person1 = " + person1);


    System.out.println("==========================================");

    //获取有参的构造函数  //public Person(String name, Integer age) 参数类型顺序要与构造函数内一致,且参数类型为字节码类型
    Constructor constructor2 = personClass.getConstructor(String.class,Integer.class);
    System.out.println("constructor2 = " + constructor2);
    //创建对象
    Object person2 = constructor2.newInstance("张三", 23);   //获取的是有参的构造方法,就必须要给参数
    System.out.println(person2);

    System.out.println("=========================================");

    //对于一般的无参构造函数,我们都不会先获取无参构造器之后在进行初始化。而是直接调用Class类内的newInstance()方法
    Object person3 = personClass.newInstance();
    System.out.println("person3 = " + person3);
    //我们之前使用的 Class.forName("").newInstance; 其本质上就是调用了类内的无参构造函数来完成实例化的
    //故可以得出结论 我们以后在使用  Class.forName("").newInstance; 反射创建对象时,一定要保证类内有无参构造函数
}
U3p4gK.png

对于getDeclaredConstructor方法和getDeclaredConstructors方法

​ 对于多出个Declared关键词的两个方法,与不带这个词的两个方法的对比。与之前叙述的一样,getDeclaredConstructor方法可以获取到任何访问权限的构造器,而getConstructor方法只能获取public修饰的构造器。具体不再测试。此外在构造器的对象内也有setAccessible(true);方法,并设置成true就可以操作了。

3.1.3 获取成员方法们Method

Method[] getMethods()

Method getMethod(String name, Class<?>... parameterTypes)

Method[] getDeclaredMethods()

Method getDeclaredMethod(String name, Class<?>... parameterTypes)

测试实体类

public class Person {

    private String name;
    private Integer age;

    //无参构造函数
    public Person() {

    }

    //有参构造函数
    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    //无参方法
    public void eat(){
        System.out.println("eat...");
    }

    //重载有参方法
    public void eat(String food){
        System.out.println("eat..."+food);
    }
}

测试invoke方法

/**
 * 3. 获取成员方法们:
 *    Method[] getMethods()
 *    Method getMethod(String name, 类<?>... parameterTypes)
 */
@Test
public void reflect5() throws Exception {
    Class personClass = Person.class;

    //获取指定名称的方法    
    Method eat_method1 = personClass.getMethod("eat");
    //执行方法
    Person person = new Person();
    Object rtValue = eat_method1.invoke(person);//如果方法有返回值类型可以获取到,没有就为null
    //输出返回值 eat方法没有返回值,故输出null
    System.out.println("rtValue = " + rtValue);

    System.out.println("--------------------------------------------");

    //获取有参的构造函数  有两个参数 第一个方法名 第二个参数列表 ,不同的参数是不同的方法(重载)
    Method eat_method2 = personClass.getMethod("eat", String.class);
    //执行方法
    eat_method2.invoke(person,"饭");

    System.out.println("============================================");

    //获取方法列表
    Method[] methods = personClass.getMethods();
    for(Method method : methods){     //注意:获取到的方法名称不仅仅是我们在Person类内看到的方法
        System.out.println(method);   //继承下来的方法也会被获取到(当然前提是public修饰的)
    }
}
U39hIs.png

测试getName方法

getName()方法获取的方法名仅仅就是方法名(不带全类名),且不带有参数列表。

public void reflect6() throws NoSuchMethodException {
    Class personClass = Person.class;
    Method[] methods = personClass.getMethods();
    for(Method method : methods){
        System.out.println(method);
        //获取方法名
        String name = method.getName();  
        System.out.println(name);   
    }
}

获取全类名

String getName() 如xx.xx.xx.Person

public void reflect7(){
    Class personClass = Person.class;
    String className = personClass.getName();
    System.out.println(className);
}

4 案例

​ 写一个"框架",不能改变该类的任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法。

4.1 配置文件实现

  1. 实体类Person

    public class Person {
    
        //无参方法
        public void eat(){
            System.out.println("eat...");
        }
    }
    
  2. 配置文件pro.properties

    .properties为扩展名的文件被认为是配置文件,支持在java.util.Properties中,以key=Value(\n)的形式配置

    className = com.test.domain.Person
    methodName = eat
    
  3. 编写测试方法

    public class ReflectTest {
    
        public static void main(String[] args) throws Exception {
    
            /**
             * 前提:不能改变该类的任何代码。可以创建任意类的对象,可以执行任意方法
             * 即:拒绝硬编码
             */
    
            //1.加载配置文件
            //1.1创建Properties对象
            Properties pro = new Properties();
            //1.2加载配置文件,转换为一个集合
            //1.2.1获取class目录下的配置文件  使用类加载器
            ClassLoader classLoader = ReflectTest.class.getClassLoader();
            InputStream is = classLoader.getResourceAsStream("pro.properties");
            pro.load(is);
    
            //2.获取配置文件中定义的数据
            String className = pro.getProperty("className");
            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);
        }
    }
    

4.2 注解实现

​ Pro.java 注释定义文件

​ 使用方式 用于类 @Pro(ClassName="全类名", FuncName = "方法名")

​ 获取注释:

Class cls = MethodFactory.class;
Pro annotation = (Pro) cls.getAnnotation(Pro.class);
String className = annotation.ClassName();
String funcName = annotation.FuncName();

<script src="https://gist.github.com/AaronFang123/51846128f7b37a919f951189e02cd467.js">
</script>

5. 反射的优点

​ 对于框架来说,是封装好的,直接用就可以了,而不能去修改框架内的代码。但如果我们使用传统的new形式来实例化,那么当类名更改时我们就要修改Java代码,这是很繁琐的。修改Java代码以后我们还要进行测试,重新编译、发布等等一系列的操作。而如果我们仅仅只是修改配置文件,就来的简单的多,配置文件就是一个实实在在的物理文件。

​ 此外使用反射还能达到解耦的效果,假设我们使用的是new这种形式进行对象的实例化。此时如果在项目的某一个小模块中我们的一个实例类丢失了,那么在编译期间就会报错,以导致整个项目无法启动。而对于反射创建对象Class.forName("全类名");这种形式,我们在编译期需要的仅仅只是一个字符串(全类名),在编译期不会报错,这样其他的模块就可以正常的运行,而不会因为一个模块的问题导致整个项目崩溃。这就是Spring框架中IOC控制反转的本质

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341