反射是什么?
反射是java语言的一个特性。但不仅仅java有这个特性,go也有。因为他们都是静态语言,缺乏很多动态特性,他们只有通过一大堆API才能反射,所以才会有比较强的反射概念。
而反射允许程序在运行时(注意不是编译的时候),对任意一个类,都能够知道这个类的所有属性和方法;对任意一个对象,都能够调用它的任意方法和属性,通过这种方式来进行自我检查并且对内部的成员进行操作。
一句话:反射就是在运行时才知道要操作的类是什么,并且在运行时可以构建任意一个类的对象,得到任意一个类的成员变量,方法的信息,甚至还能调用他们。
反射工作原理
JVM加载类的流程
首先,我们需要搞清楚JVM加载类的流程,这里只展示与反射相关的内容。
当我们写完一个Java项目后,java文件会被编译成一个.class文件(以User.java为例) 。
.class文件在程序运行时会被ClassLoader加载到JVM中,当一个类被加载以后,JVM就会在内存中自动产生一个Class对象。
当我们以new的形式创建对象时,实际上就是通过这些Class来创建的,只不过这个class文件是编译的时候就生成的。
什么叫运行时
字节码文件要被运行,需要先通过类加载器加载,然后由JVM进行操作(比如new对象),操作的这个时间节点就叫运行时
为什么要用反射?
反射机制主要提供了以下功能:
- 实例化任意一个类的对象
- 调用任意对象的方法
- 判断任意一个对象所属的类
- 判断任意一个类所具有的成员变量和方法;
- 获取任意对象的属性,并且能改变对象的属性
- 获取任意类的名称、package信息、所有属性、方法、注解、类型、类加载器等
- 通过反射我们还可以实现动态装配,降低代码的耦合度,动态代理等。
反射的作用
在不使用反射时,构造对象使用 new 方式实现,需要明确指定类名,这种方式在编译期把对象的类型确定下来,属于硬编码实现;
而在使用反射的时候,在程序运行过程中确定和解析数据类的类型,只需传入类名参数,就可以生成对象
比较典型的例子是:原生的JDBC中加载驱动类。
我们项目的数据库有时是用MySQL,有时用Oracle,需要动态地根据实际情况加载驱动类,这个时候反射就有用了。
我们在使用 JDBC 连接数据库时使用 Class.forName()通过反射加载数据库的驱动程序,如果是MySQL则传入MySQL的驱动类,而如果是Oracle则传入的参数就变成另一个了。
反射的优缺点
优点:
(1)能够运行时动态获取类的实例,大大提高系统的灵活性和扩展性。
(2)与Java动态编译相结合,可以实现无比强大的功能
(3)降低代码之间的耦合度
缺点:
(1)使用反射,效率会降低(实例化对象的时间相对于使用new
时要长一些),使用不当会造成很高的资源消耗
(2)使用反射相对来说不安全
(3)破坏了封装性,可以通过反射获取这个类的私有方法和属性
任何事物,都有两面性。反射的优点也是它的缺点,所以使用时,一定要注意具体的应用场景
我们知道反射机制允许程序在运行时取得任何一个已知名称的class的内部信息,包括包括其modifiers(修饰符),fields(属性),methods(方法)等,并可于运行时改变fields内容或调用methods。那么我们便可以更灵活的编写代码,代码可以在运行时装配,无需在组件之间进行源代码链接,;还有动态代理的实现等等
怎么使用反射?
反射相关的类
与反射相关的类主要有四个,如表格所示
类名 | 描述 |
---|---|
Class | 代表类的实体,在运行的Java程序中表示类和接口 |
Field | 代表类的成员变量 |
Method | 代表类的成员方法 |
Constructor | 代表类的构造方法 |
我们可以通过Class对象获取其对应的Field/Constructor/Method 对象
这里有大量方法提供给我们使用,本文只列举一些常用和较为重要的,剩下的可以通过API手册类比学习,API手册已放在评论区,可自取。
Class
任何运行在内存中的所有类都是该 Class 类的实例对象,每个 Class 类对象内部都包含了本来的所有信息。记着一句话,通过反射干任何事,先找 Class 准没错!
创建Class对象
- 通过静态方法 Class.forName("全类名"); 即包名+类名
Class<User> clazz = Class.forName("com.andrew.reflection.User");
- 直接 类名.class
Class<User> clazz = User.class;
- 通过对象反过来拿到Class对象
User user=new User();
Class clazz = User.getClass();
创建实例对象
通过 Class 对象的 newInstance() 方法(调用的是无参构造器)
Class clazz = Class.forName("com.andrew.reflection.User");
User user= (User)clazz.newInstance();
通过 Constructor 对象的 newInstance() 方法(可以选择特定构造方法)
Class clazz = Class.forName("com.andrew.reflection.User");
Constructor constructor = clazz.getConstructor();
User user= (User)constructor.newInstance();
//调用一个有参构造,进行类对象的初始化
Class clazz = Class.forName("com.andrew.reflection.User");
Constructor constructor = clazz.getConstructor(String.class);
User user= (User)constructor.newInstance("andrew");
获取类名
方法 | 描述 |
---|---|
getName(); | 获取全类名 |
getSimpleName(); | 获取简单类名 |
获取成员变量
方法 | 描述 |
---|---|
getField("属性名") | 获得单个public的Field对象 |
getFields() | 获得所有public的Field对象(数组形式返回) |
getDeclaredField("属性名") | 获得单个Field对象:共有+私有 |
getDeclaredFields() | 获得类的所有Field对象(包括private 声明的和继承自父类的)(数组形式返回) |
获取成员方法
方法 | 描述 |
---|---|
getMethod(String name, Class[] parameterTypes) | 获得类的特定方法;name 指定方法的名字,parameterTypes 指定方法的参数类型(不定长,区分重载) |
getMethods() | 输出的数组包括所有public的有父子继承关系的类的方法 |
getDeclaredMethod(String name, Class[] parameterTypes) | 获得类的特定方法;参数同第一个 |
getDeclaredMethods() | 获得类的所有方法 (输出的数组中不含构造方法,只能拿到本类的方法public+private) |
获取构造方法
方法 | 描述 |
---|---|
getConstructor(Class...<?> parameterTypes) | 获得对应的类的public的构造方法,parameterTypes 指定构造方法的参数类型(不定长参数) |
getConstructors() | 获得该类的public的构造方法。 |
getDeclaredConstructor(Class...<?>parameterTypes) | 获得该类中与参数类型匹配的构造方法 |
getDeclaredConstructors() | 获得该类所有构造方法 |
Field
描述一个类的属性,内部包含了属性对象的所有信息,例如数据类型,属性名,访问修饰符······
如何初始化对象中的私有属性
1.获得setter方法
//获得User的Class对象
Class<User> user=User.class;
//通过无参构造实例化一个user对象
User instance = user.newInstance();
//通过调用setter方法(声明为public)进行赋值操作
user.getMethod("setName",new Class[]{String.class}).invoke(instance,"张三");
2.破坏封装性 .setAccessible(boolean flag);(flag为true时,可直接赋值;为false时,关闭访问权限)
//获得User的Class对象
Class<User> user=User.class;
//通过无参构造实例化一个user对象
User instance = user.newInstance();
//获取User类中私有化的属性 name
Field name = user.getDeclaredField("name");
//为 true 时,可直接访问
name.setAccessible(true);
//对name进行赋值,参数一是实例化的对象,参数二是要赋的实例化对象的属性值
name.set(instance,"zhangsan");
Method
描述一个类的所有方法(包括抽象方法),内部包含了该方法的所有信息,与Constructor
类似,不同之处是 Method 有返回值类型信息,因为构造方法是没有返回值的。
这里只讲述invoke()方法,其他方法可见使用手册
java提供invoke()方法,在运行时根据业务需要调用相应的方法。
例如:
//获得User类的Class对象
Class userClass = User.class;
//获得无参构造器
Constructor constructor = userClass.getConstructor();
//获得 eat 方法,并传入一个数组(其中只有一个类型为 int 的参数)
Method eatMethod = userClass.getMethod("eat", new Class[]{int.class});
//执行 eat 的方法,参数一是通过无参构造实例化的一个对象 参数二是 eat 方法的方法参数
eatMethod.invoke(constructor.newInstance(), 2);
invoke方法有两个参数,第一个参数是要调用方法的对象,上面的代码中就是User的对象,第二个参数是调用方法要传入的参数。如果有多个参数,则用数组。
如果调用的是static方法,invoke()方法第一个参数就用null代替;
Constructor
描述一个类的构造方法,内部包含了构造方法的所有信息,例如参数类型,参数名字,访问修饰符······
各位掘友可以根据上面讲述的内容类比学习,这里就不赘述了。
反射与Spring
反射之于Spring,相当于鸡蛋之于蛋炒饭。可以说Spring就是建立在反射机制上的,在学习使用Spring之前,很有必要深刻理解反射的概念。
下面只是简单列举反射在Spring中的应用场景
如Spring的动态代理
- 据库连接和事务管理。(Spring有一个事务代理,可以启动和提交/回滚事务)
- 用于单元测试的动态模拟对象
- 类似AOP的方法拦截。
又如Spring 的 IOC(动态加载管理 Bean),Spring通过配置文件配置各种各样的bean,需要用到哪些bean就配哪些,Spring容器就会根据实际需求去动态加载,程序就能健壮地运行。
除此之外还有很多框架:mybatis、dubbo、rocketmq等等都会用到反射机制。
END
反射是java非常重要的特性,希望掘友们看完本文后,对其能有一个清晰的认识。本人水平有限,不免有所遗漏,可以在评论区留言,互相交流。