在云笔记项目中, 补充了部分反射的知识, 反射这一部分基础知识非常重要, 前面学习的框架 Spring 和 MyBatis 读取 xml 配置文件创建对象, 以及 JDBC 加载驱动等都用了反射, 但只知道有这个东西, 具体不知道怎么用, 大概的原理是怎么样的, 现在简单的记录下
什么是反射
反射 (Reflection) 是 Java 提供的动态执行机制, 可以动态加载类, 动态创建对象, 动态获取类信息, 比如接口信息, 方法信息, 属性信息, 构造信息等. 并可以通过获取到的信息动态创建对象, 动态调用方法等. 如果想更好的感受反射, 可以从静态执行方式和动态执行方式两种去对比.
a. 静态执行: Java 代码通过编译以后就确定的执行次序, 称为静态执行次序. 比如新建一个对象 Foo foo=new Foo(), 然后调用对象的方法 foo.test()执行, 这就是静态执行, 代码在编译期就知道具体的对象是什么, 对象要执行的方法是什么.
b. 动态执行: 在运行期间才确定要创建哪个类, 执行类的哪个方法. 也就是编译期无法得知具体的类信息, 也无法得到类中的方法信息, 等具体加载类执行时才能知道. Java 反射 API, 可以实现动态执行加载类和执行方法的功能.
反射的核心是 JVM 在运行时才动态加载类或调用方法 / 访问属性, 它不需要事先 (写代码的时候或编译期) 知道运行对象是谁.
为了更好理解反射, 先准备两个写好的类, 后面做用素材测试使用:
Foo 类:
- package Test;
- public class Foo {
- // 加几个属性, 演示通过反射得到所有的属性
- public String name;
- public int age;
- private int salary;
- // 加一个构造器, 演示通过反射获得构造器
- // public Foo(String name) {
- // super();
- // this.name = name;
- // }
- // 加几个方法, 演示通过反射动态调用方法
- private String getPrice() {
- return "100";
- }
- public String hello() {
- return "hello reflect";
- }
- }
Too 类:
- package Test;
- public class Too {
- public String name;
- public int age;
- private int salary;
- public String Hello() {
- System.out.println("Hello Too");
- return "Success";
- }
- }
反射功能
a. 动态加载类, 主要有三种方法:
(1)使用 Class 类的 forName()静态方法
作用是将类名对应的类加载到方法区, 如果类名错误就抛出异常.
forName()工作原理: 写好一个类, 比如 Foo.java 类, 通过编译后会生成 Foo.class 字节码文件, 当执行 Class cls=Class.forName("Foo")后, Class.forName()这个方法会首先读取 Foo.class 字节码文件, 将其加载到方法区中. Class.forName()执行完后的返回值就是一个具体的 Class 类型对象, 储存在堆中, 其通向方法区. 而 cls 是一个指向具体对象的引用, 会储存在栈中. 通过 cls, 可以获取 Foo 类的一切信息, 属性和方法等. 当 forName()方法中的类名更改后, 其加载新的类对应的字节码文件到方法区, 从而实现了类的动态加载.
- package Test;
- import java.lang.reflect.Constructor;
- import java.lang.reflect.Field;
- import java.lang.reflect.InvocationTargetException;
- import java.lang.reflect.Method;
- import java.util.Scanner;
- public class Demo {
- public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
- // 动态加载类
- Scanner scan=new Scanner(System.in);
- // 得到用户输入的类名
- System.out.println("请输入类名:");
- String className=scan.nextLine();
- // 动态加载类
- Class cls=Class.forName(className);
- System.out.println("--------------- 类信息 ---------------");
- System.out.println(cls);// 输出 class 包名. 类名
- System.out.println(cls.getName());// 输出包名. 类名
- }
- }
控制台输入 Test.Foo 后, 输出结果如下, getName()获得的是 Java 内部使用的真正名称, 包含包名和类名, 其他还有 getSimpleName()和 getPackage()方法等, 分别代表返回不包含包名和只包含包名的信息:
(2)直接获取某一个对象的 class
1 Class<Date> cls = Date.class;
获取 Class 对象如果提前知道了类名, 不一定需要实例对象, 可以使用<类名>.class 获取 Class 对象.
(3)调用某个对象的 getClass()方法
- StringBuilder str = new StringBuilder("123");
- Class<?> cls = str.getClass();
所有类的根父类 Object 有一个方法 public final native Class<?> getClass(), 可以获取对象的 Class 对象. Class 是一个泛型类, 使用 getClass()方法时并不知道返回的具体类是什么类型, 因此返回 Class<?>. 问号 "?" 代表类型的实参, 不是类型形参, 其代表所有类型的父类, 是一种实际的参数.
b. 动态创建对象, 主要有两种方法
(1)可以使用 cls.newInstance()方法创建对象, 相当如使用无参构造器创建了对象.
Object obj=cls.newInstance()
特点: 动态创建对象; 可以创建任何对象; cls 对应的类必须有无参数构造器, 如果没有将抛出异常,(一般符合 javabean 规范的类都有无参数构造器)
(2)使用 cls.getConstructor()方法得到 Constructor 对象, 然后使用 Constructor.getInstance()方法获取对象, 这个构造器可以传入参数, 是跟第一种主要的区别.
在上述 main 方法中加上如下代码获取对象, 注释的部分是通过带参数的构造器创建对象:
- // 动态创建对象
- //1 使用 cls.newInstance()来获取对象, 使用无参数构造器
- Object obj=cls.newInstance();
- System.out.println("--------------- 对象信息 ---------------");
- System.out.println(obj);
- //2 使用 cons.newInstance()获取对象, 使用特定的构造器
- // Constructor cons=cls.getConstructor(String.class);
- // Object obj=cons.newInstance("clyang");
- // System.out.println(obj);
同样控制台使用 Test.Foo 类进行测试, 输出结果为:
c. 反射可以查找类中的方法
可以返回类中声明的全部方法信息, eclipse 开发工具中, 当得到一个对象后, 可以通过输入点, 就可以列出对象里所有的方法, 其实也是用的反射机制. 如 Foo foo=new Foo(), 当输入 foo. 时,"foo." 后面会列出一堆方法和属性信息, 其实就是在点了后, java 获得了 foo, 然后获取到了类名, 通过 java 反射得到这个类下的方法和属性, 然后将其输出到界面, 列出来展示给开发人员.
(1)getDeclaredMethods()
在上述 main 方法中加上如下代码获取所有声明的方法, 使用 getDeclaredMethods()方法返回类或接口声明的所有方法, 包括公共, 保护, 默认 (包) 访问和私有方法, 但不包括继承的方法:
- // 动态检查类中声明的方法信息
- Method[] method=cls.getDeclaredMethods();// 返回所有 private,public,protected 等修饰的方法
- System.out.println("--------------- 方法信息 ---------------");
- for(Method m:method) {
- System.out.println(m);// 输出方法信息
- }
同样使用 Test.Foo 类进行测试, 控制台输出结果为:
(2)getMethods()
方法返回某个类的所有公用 (public) 方法, 包括其继承类的公用方法.
(3)getMethod(String name,Class<?>... parameterTypes)
方法返回一个特定的方法, 其中第一个参数为方法名称, 后面的参数为方法的参数对应 Class 的对象
d. 动态执行方法
(1) 需要在动态创建对象后再动态执行方法
Object obj=cls.newInstance();// 动态创建对象
(2)找到对象对应的类型方法信息, 方法信息在类上查找
method m=cls.getDeclaredMethod(name);// 找到对象对应类 cls 中的方法信息
(3)接下来就可以调用方法了
- m.invoke(obj);// 字面意思是, m 方法调用了 obj, 个人猜测是 m 方法唤醒了 obj, 然后底层调用了 obj.m()方法.
- // 动态调用方法
- System.out.println("请输入方法名");
- String methodName=scan.nextLine();
- Method m=cls.getDeclaredMethod(methodName);
- // 如果想让 private 方法也能被调用, 需要加上
- System.out.println("--------------- 动态执行方法 ---------------");
- m.setAccessible(true);
- Object o=m.invoke(obj);
- System.out.println(o);
同样使用 Test.Foo 类进行测试, 测试执行 hello 方法, 控制台输出结果为:
e. 反射也可以查找类中的属性
(1)getDeclaredFields()
可以返回类所有已声明的成员变量, 但不能得到其父类的成员变量, Field[] field=cls.getDeclaredField().
- // 返回类的所有成员变量
- Field[] fields=cls.getDeclaredFields();
- System.out.println("--------------- 属性信息 ---------------");
- for(Field f:fields) {
- System.out.println(f);
- }
同样使用 Test.Foo 类进行测试, 控制台输出结果可以看出私有的成员变量也可以得到:
(2)getField
访问共有的成员变量.
f. 反射可以获取类中的构造器
可以获取类中所有声明的构造器, Constructor[] constructor=cls.getDeclaredConstructors();
- // 返回类中声明的构造器
- Constructor[] constructor=cls.getDeclaredConstructors();
- System.out.println("--------------- 构造器信息 ---------------");
- for(Constructor c:constructor) {
- System.out.println(c);
- }
同样控制台使用 Test.Foo 类进行测试, 输出结果为:
发现输出的是默认构造器方法, 因为在 Foo 类中, 没有写构造器, 因此创建对象时默认调用了无参数构造器.
g. 反射的用途
(1)Eclipse 快捷菜单使用了反射, 利用反射发现类的属性和方法
(2)Spring 利用了反射: 动态加载类, 动态创建 bean, 动态注入属性, 包括私有属性注入, 动态解析注解
(3)Mybatis 利用了反射, 查询时候, 动态将查询结果利用反射注入到 bean 并返回
(4)Junit 使用了反射
(5)注解的解析使用了反射
(6)Servlet 调用使用了反射
总结:
反射内容非常复杂, 现在只是学习如何基本的使用, 具体底层的实现, invoke 方法的原理, 还需要后续学习补充.
参考博客:
参考博文: https://www.cnblogs.com/coprince/p/8603492.html
参考书籍:《Java 编程的逻辑》
来源: http://www.bubuko.com/infodetail-2985613.html