【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
www.cnblogs.com/cnb-yuchen/...出自【进步*于辰的博客】
参考笔记一,P74.6;笔记二,P74.2、P75.3;笔记三,P15.2、P43.2、P44.2/3。
目录
- 
- [1.1 概述](#1.1 概述 "#11-%E6%A6%82%E8%BF%B0")
- [1.2 反射的另一种情形](#1.2 反射的另一种情形 "#12-%E5%8F%8D%E5%B0%84%E7%9A%84%E5%8F%A6%E4%B8%80%E7%A7%8D%E6%83%85%E5%BD%A2")
- [1.3 扩展:静态内部类的类加载](#1.3 扩展:静态内部类的类加载 "#13-%E6%89%A9%E5%B1%95%E9%9D%99%E6%80%81%E5%86%85%E9%83%A8%E7%B1%BB%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD")
 
- 
- 
[2.1 构造方法](#2.1 构造方法 "#21-%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95") - [2.1.1 获取构造方法数组](#2.1.1 获取构造方法数组 "#211-%E8%8E%B7%E5%8F%96%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%E6%95%B0%E7%BB%84")
- [2.1.2 获取指定构造方法](#2.1.2 获取指定构造方法 "#212-%E8%8E%B7%E5%8F%96%E6%8C%87%E5%AE%9A%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95")
- [2.1.3 构造实例](#2.1.3 构造实例 "#213-%E6%9E%84%E9%80%A0%E5%AE%9E%E4%BE%8B")
 
- 
[2.2 方法](#2.2 方法 "#22-%E6%96%B9%E6%B3%95") - [2.2.1 获取方法](#2.2.1 获取方法 "#221-%E8%8E%B7%E5%8F%96%E6%96%B9%E6%B3%95")
- [2.2.2 一个特例:通过反射调用 main()](#2.2.2 一个特例:通过反射调用 main() "#222-%E4%B8%80%E4%B8%AA%E7%89%B9%E4%BE%8B%E9%80%9A%E8%BF%87%E5%8F%8D%E5%B0%84%E8%B0%83%E7%94%A8-main")
 
- 
[2.3 变量](#2.3 变量 "#23-%E5%8F%98%E9%87%8F") 
 
- 
- 
- [4.1 降低代码注入](#4.1 降低代码注入 "#41-%E9%99%8D%E4%BD%8E%E4%BB%A3%E7%A0%81%E6%B3%A8%E5%85%A5")
- [4.2 跳过泛型检查](#4.2 跳过泛型检查 "#42-%E8%B7%B3%E8%BF%87%E6%B3%9B%E5%9E%8B%E6%A3%80%E6%9F%A5")
 
1、什么是"反射"?
关于类加载,详述可查阅博文《Java知识点锦集》的第5项。
1.1 概述
大家先看一个图,

过程说明:
- A → B。当JVM运行,将 java 源文件编译成 class 字节码文件。
- B → D。执行如下代码,通过类加载器 ClassLoader 将 class 字节码文件加载进JVM方法区 、生成 class 信息、进而创建 Class 对象,这个过程就是"类加载"。(注:只有对类的主动使用才会触发类加载,例如:反射、实例化)。
            
            
              java
              
              
            
          
          1、A.class
2、new A().getClass()
3、Class.forName()- D → E。通过调用newInstance(),使用 Class 对象创建实例。
结论:反射是一种通过类加载加载JVM方法区中的 class 信息、创建实例的机制。
补充一点:
class字节码文件(图中B)中包含字面量和符号引用。字面量指为变量所赋的值;符号引用指变量在编译时的一个地址标识 ,不是确切的地址,因为只有在运行时,才会为变量分配内存地址。
1.2 反射的另一种情形
            
            
              java
              
              
            
          
          1、A.class// A 是类名
2、new A().getClass()
3、Class.forName()在上文中说道,过程 B → D 就是类加载,执行如上代码中任一条都可以触发此过程。
创建 Class 对象是反射的标志,而反射基于类加载。因此,这三种情况都属于反射。可实际上,只有第3种才会触发类加载。听我细细道来。。。
大家先看个图。

反射的最终目的是实例,可有时候只是为了获取 Class 对象。若已存在实例,则通过调用getClass()获取会更简便。
这种通过对实例进行反编译、进而创建 Class 对象的机制也属于反射,
可这种情形不会触发类加载,因为类加载只会执行一次,既然存在实例,自然已完成了类加载。
我为何会注意到"反编译不会触发类加载"这一细节?
平日看源码的时候,经常会看到这样的代码块:
            
            
              java
              
              
            
          
          static {}这个叫做"静态代码块",它执行于类初始化时(类加载的第三过程)。在这里会编写一些为类变量赋初始值或初始操作的代码,而往往这些代码并不容易看懂,那就需要debug。(PS :进行debug前当然需要先知道什么情况下才会执行"静态代码块")
总结 :只有Class.forName()和 实例化 才会触发类加载,而getClass()不会。并且,通过debug发现,A.class也同样不会触发类加载,故可判断A.class也是通过反编译进行反射。
1.3 扩展:静态内部类的类加载
大家看一个栗子。
            
            
              java
              
              
            
          
          class OuterClass {
    static class InnerClass {
        static {
            sout "csdn";
        }
    }
}什么情况下才会打印"csdn"?据上文可知,只要进行类加载,就会执行static {}。
虽然内部类属"懒加载",但其类加载在本质上与外部类的类加载相同,即当执行Class.forName()或实例化时才会触发类加载。如下述代码:
            
            
              java
              
              
            
          
          1、Class z1 = Class.forName("OuterClass$InnerClass");
2、OuterClass.InnerClass obj1 = new OuterClass.InnerClass();补充说明:为什么不能在类方法中实例化非静态内部类,而静态内部类可以?
因为类方法加载于类加载时,而非静态内部类属"懒加载",在外部类调用时才加载。换言之,类加载时不会加载非静态内部类(可视为不存在),自然无法实例化。
而静态内部类同外部类一起加载(可视为"积极加载"),自然可以实例化。
PS :
可能大家会疑惑,为什么我不对其他几种内部类的类加载进行说明?原因:
- 关于其他几种内部类的类加载我暂未研究;
- 只有静态内部类内才能定义static {}。
具体原因可查阅博文《Java知识点锦集》的第15项。
2、反射运用(获取类成员)
在反射的使用中,直接涉及的类是 Class
<T>。以下3个方法可用于获取构造方法、方法(包括成员方法、类方法)和变量(包括成员变量、类变量)。
            
            
              java
              
              
            
          
          getConstructor(xx)	// 获取构造方法,xx是构造方法形参的数据类型的class
getMethod(a, b)		// 获取方法,包括成员方法和类方法,a是方法名,b是方法形参的数据类型的class集,b位置是可变参数
getField(xx)	// 获取变量,包括成员变量和类变量,xx是变量名笼统列举,大家看起来有点云里雾里,下面一一详述。
2.1 构造方法
待反射类:
            
            
              java
              
              
            
          
          @Data
class Reflect {
    private int x;
    private String y;
    private boolean z;
 
    private Reflect() {
    }
    Reflect(int x) {
    	this.x = x;
    }
    protected Reflect(int x, String y) {
		this.x = x;
		this.y = y;
    }
    public Reflect(int x, String y, boolean z) {
   		this.x = x;
		this.y = y;
		this.z = z;
    }
}2.1.1 获取构造方法数组
测试示例:
            
            
              java
              
              
            
          
          class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("com.neusoft.boot.Reflect");
        // 获取公共(public)构造方法集合
        Constructor[] arr1 = class1.getConstructors();
        System.out.println("公共(public)构造方法:");
        for (Constructor c : arr1) {
            System.out.println(c);
        }
        
        // 获取所有构造方法集合
        Constructor[] arr2 = class1.getDeclaredConstructors();
        System.out.println("所有构造方法:");
		for (Constructor c : arr2) {
            c.setAccessible(true);// 强制访问
            System.out.println(c);
        }
    }
}测试结果:

2.1.2 获取指定构造方法
相应获取方法:
            
            
              java
              
              
            
          
          1、getConstructor(xx)	// 获取公共(public)构造方法
2、getDeclaredConstructor(xx)	//  获取构造方法,包括:private、默认(未指定访问修饰符或类中未自定义构造方法)、protected、public测试示例:
            
            
              java
              
              
            
          
          class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("com.neusoft.boot.Reflect");
        Constructor privateC = class1.getDeclaredConstructor(null);// 获取私有构造方法
        privateC.setAccessible(true);
        System.out.println("私有构造方法:");
        System.out.println(privateC);
        Constructor defaultC = class1.getDeclaredConstructor(int.class);// 获取访问修饰符为"默认"的构造方法
        System.out.println("访问修饰符为"默认"的构造方法:");
        System.out.println(defaultC);
        Constructor protectedC = class1.getDeclaredConstructor(int.class, String.class);// 获取访问修饰符为"protected"的构造方法
        System.out.println("访问修饰符为"protected"的构造方法:");
        System.out.println(protectedC);
        Constructor publicC1 = class1.getDeclaredConstructor(int.class, String.class, boolean.class);// 获取访问修饰符为"public"的构造方法
        System.out.println("访问修饰符为"public"的构造方法:");
        System.out.println(publicC1);
        Constructor publicC2 = class1.getConstructor(int.class, String.class, boolean.class);// 获取访问修饰符为"public"的构造方法
        System.out.println("访问修饰符为"public"的构造方法:");
        System.out.println(publicC2);
    }测试结果:

由于构造方法名称固定,故在获取构造方法时,只需要指定相应构造方法所有形参的 Class 对象即可。
2.1.3 构造实例
测试示例:
            
            
              java
              
              
            
          
          class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("com.neusoft.boot.Reflect");
        Constructor publicC1 = class1.getDeclaredConstructor(int.class, String.class, boolean.class);// 获取访问修饰符为"public"的构造方法
        System.out.println("访问修饰符为"public"的构造方法:");
        System.out.println(publicC1);;
        Reflect d1 = (Reflect)publicC1.newInstance(10, "yc", true);// 构造方法实例化
        System.out.println("一个Reflect对象;");
        System.out.println(d1);
    }
}测试结果:

2.2 方法
2.2.1 获取方法
相应获取方法:
            
            
              java
              
              
            
          
          1、getMethod(a, b)	// 获取公共(public)方法
2、getDeclaredMethod(a, b)	//  获取方法,包括:private、默认(未指定访问修饰符)、protected、public待反射类:
            
            
              java
              
              
            
          
          class Reflect {
    private void print(String msg) {
        System.out.println("打印信息:" + msg);
    }
    public static void main(String[] args) {
        System.out.println(Arrays.toString(args));
    }
}测试示例:
            
            
              java
              
              
            
          
          class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("com.neusoft.boot.Reflect");
        Constructor publicC = class1.getDeclaredConstructor(null);// 获取默认构造方法
        Reflect o1 = (Reflect)publicC.newInstance(null);// 实例化
        Method m1 = class1.getDeclaredMethod("print", String.class);	// 获取方法名为print,具有一个String类型参数的方法
        m1.setAccessible(true);// 强制访问
        m1.invoke(o1, "反射方法测试");
    }
}测试结果:

在示例中,先通过反射获取默认无参构造方法(由JVM提供,因为未自定义构造方法),再调用newInstance(null)创建实例(因为构造方法无参,故无实参 ,为null)。
由于方法可重载 ,故获取方法时,需要指定方法名和所有形参的 Class 对象。
代码中的invoke()的作用是调用方法。
注意,
- 若方法是成员方法,则第一个参数是对象,表示调用哪个对象的成员方法;第二个参数是实参集;
 注:方法形参不一定只有一个,因此实参集中实参可能有多个。实参集的写法类似可变参数。
- 若方法是类方法,由于类方法属于类,不属于对象,故不需要指定对象,因此第一个参数为 null;第二个参数同上。
2.2.2 一个特例:通过反射调用 main()
看下述代码:
            
            
              java
              
              
            
          
          class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("com.neusoft.boot.Reflect");
        Method m2 = class1.getMethod("main", String[].class);
        System.out.println("调用main()");
        m2.invoke(null, (Object) new String[]{"a", "b"});
    }
}对于以此情形调用main()是否会重新启动了一个JVM,暂未深究。
注意:
若方法形参类型为数组 ,如上述main(),在调用invoke()时,实参必须强转为Object。
2.3 变量
相应获取方法:
            
            
              java
              
              
            
          
          1、getField(xx)	// 获取公共(public)变量
2、getDeclaredField(xx)	//  获取变量,包括:private、默认(未指定访问修饰符)、protected、public待反射类:
            
            
              java
              
              
            
          
          class Reflect {
    private static String name;
    private int age; 
}测试示例:
            
            
              java
              
              
            
          
          class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("com.neusoft.boot.Reflect");
        Constructor publicC = class1.getDeclaredConstructor(null);// 获取默认构造方法
        Reflect o1 = (Reflect)publicC.newInstance(null);// 实例化
        Field ageField = class1.getDeclaredField("age");// 获取名为age的变量
        ageField.setAccessible(true);
        ageField.set(o1, 100);// 为对象o1的变量age赋值
        System.out.println("打印对象:");
        System.out.println(o1);
        Object value = ageField.get(o1);// 获取对象o1的变量age值
        System.out.println("对象o1的变量age的值为:");
        System.out.println(value);
    }
}测试结果:

由于变量具有唯一性,故只需要指定变量名。
赋值和获取。
            
            
              java
              
              
            
          
          ageField.set(o1, 100);// 为对象o1的变量age赋值
Object value = ageField.get(o1);// 获取对象o1的变量age值成员变量、类变量的赋值和获取与成员方法、类方法类似,故不赘述。
3、运用反射时的注意事项
1 、若获取的类成员由非public修饰,则存在访问限制 ,在执行功能前,必须先调用xx.setAccessible(true),目的是设置为允许强制访问 。特别的:在默认情况下,私有成员不允许访问。(坦白:我忘了最后这一点的出处,所以暂且只能作为一个结论)
2 、当通过newInstance()实例化时,若调用的构造方法为无参构造方法,括号内可为null或空。
3 、无法通过使用子类的 Class 对象进行反射获取任何父类成员,父类同样。
其中缘由:
- 子类可访问父类所有成员,而并非拥有;
- 在JVM内存空间的堆 中,父类初始化数据存储于子类内存空间。而反射执行的位置是在方法区,自然无法获取到父类成员。
详述可查阅博文《Java知识点锦集》的第5.4项、第8项。
一种特殊情况:
当父类的成员变量或成员方法以public修饰时(没有其他修饰符),通过getField()/getMethod()可获取。
难道真的没办法获取父类成员?
当然不是。无论 Class 对象还是实例,有一点是确定的:子类可访问父类成员 。那么,就可以从此处着手。
具体办法:(目前仅限于获取父类变量。至于其他成员,由于实用性不大,故暂不探讨)
- 办法一:将父类变量作为子类方法的返回值;
- 办法二:先获取父类的 Field 对象,调用get()时,传入子类实例。
4、通过反射无法获取抽象类或接口的方法。
5 、一个误区 :定义方法void get(Object obj) {},调用时,实参类型可以任意,但当通过class.getMethod("get", xx)获取此方法时,xx只能是Object.class,因为每个类的 Class 对象唯一且不存在继承关系。
6 、获取内部类的 Class 对象,需使用特殊符号$。
示例:(获取ArrayList<E>类的嵌套类-迭代器类Itr的 Class 对象)
            
            
              java
              
              
            
          
          1、Class.forName("java.util.ArrayList$Itr");	√
2、Class.forName("java.util.ArrayList.Itr");	×7 、一个结论 :反射的本质其实就是加载类的 Class 信息、生成 Class 对象的过程。类与类之间可能存在关联,如:包含、继承或依赖等,但类的 Class 信息一定是唯一且独立 的。因此,无法通过一个类的 Class 对象获取另一个类的成员。
对于在第3点中提到:"子类可以通过getField()/getMethod()获取父类成员变量和成员方法",那是因为这2个方法的底层存在父类递归机制 (从源码中获知,具体待明)。
注意:构造方法没有此性质。
4、列举两个反射在实际开发中的运用
4.1 降低代码注入
上文中阐述的各种获取类成员的方法的实参都是"写死"在程序中的,代码注入性太强。
什么是"代码注入"?
大家可能是第一次听说这个概念,比较抽象,不容易理解,我举个例:类A通过反射获取类B的变量、方法等,其中,类B的全限定名 、变量名 、方法名 等都"写死"。可现在类B的各种类信息改了。那么,你就需要去看懂类A中反射那部分代码,然后一一进行修改,是不是很耗费时间、精力。这就是"代码注入性"太强。
通过反射降低代码注入性的方法:
用配置文件封装各种实参,修改时可以统一修改,且不需要考虑代码细节。
测试示例:
待反射类:
            
            
              java
              
              
            
          
          class Reflect {
    private void print(String msg) {
        System.out.println("打印信息:" + msg);
    }
}配置文件:
            
            
              java
              
              
            
          
          classPath=com.neusoft.boot.Reflect	// 类全限定名
methodName=print	// 方法名测试类:
            
            
              java
              
              
            
          
          class TestReflect {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName(getConfig("classPath"));
        Constructor publicC = class1.getDeclaredConstructor(null);// 获取默认构造方法
        Reflect o1 = (Reflect)publicC.newInstance(null);// 实例化
        Method m1 = class1.getDeclaredMethod(getConfig("methodName"), String.class);	// 获取方法名为print,具有一个String类型参数的方法
        m1.setAccessible(true);// 强制访问
        m1.invoke(o1, "反射降低代码注入性测试");// 输出结果【打印信息:反射降低代码注入性测试】
    }
    /**
     * 获取配置
     *
     * @param key
     * @return
     */
    private static String getConfig(String key) throws Exception {
        Properties p = new Properties();
        // 配置类Properties加载配置文件的方法很多,这里举个例
        String filePath = "G:\\projects-local\\java\\boot-demo\\src\\main\\resources\\Reflect-confg.properties";
        p.load(new FileReader(filePath));
        return p.getProperty(key);
    }
}4.2 跳过泛型检查
一个大家看过无数次的例子:
            
            
              java
              
              
            
          
          class TestReflect {
    public static void main(String[] args) throws Exception {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add("4");// 编译错误
    }
}在编译时,JVM会进行泛型检查,目的是判断所赋的值或加入的值的类型是否与类型实参 相同。
反射的底层机制是类加载,不经过编译,故可以跳过泛型检查。
示例 :运用反射向List<Integer>集合内添加字符串。
            
            
              java
              
              
            
          
          class TestReflect {
    public static void main(String[] args) throws Exception {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        Class listClass = list.getClass();
        Method addMethod = listClass.getMethod("add", Object.class);// 反射获取方法,与泛型的具体类型无关,所以是Object
        addMethod.invoke(list, 4);// 成功
        addMethod.invoke(list, "5");// 成功
        addMethod.invoke(list, "5ab");// 成功
        System.out.println(list);// 打印:【1,,2, 3, 4, 5, 5ab】
    }
}为什么List<Integer>可以存放字符串?
关于泛型,推荐一位前辈的博文《java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一》(转发) 。
如果大家对那篇博文中的一些概念晦涩不清,可以浏览一下我写的这篇文章《关于对Java泛型的理解与简述(读后简结)》。
无论是泛型接口、泛型类,亦或者泛型方法,泛型的限制作用都在于泛型检查 ,作用于编译阶段 ,例如上述的addMethod.invoke(list, "5ab"),是通过反射获取的 Method 对象,直接将字符串"5ab"加入到list中,不经过编译,故跳过了泛型检查。
5、最后
本文中的例子是为了方便大家理解、以及阐述如何通过反射获取类成员而简单举例的,不一定有实用性。大家在实际编程中可以尝试用反射去解决问题,有些情况下会简便许多。
之前,我用反射实现过"不同类之间属性值传递 "(因为这两个类有几个属性相同或有某种规律,如果逐个get()/set(),代码太冗余、质量和效率都不高)。
本文完结。