Java中的反射
前言
反射机制是Java的一个重要特性,反射机制能够在程序处于运行状态的时候获取任意一个类的属性和方法。所以反射被称为java框架的灵魂。
反射的重要组成部分
Class对象: 任何运行在内存中的所有类都是该 Class 类的实例对象,每个 Class 类对象内部都包含了实例的所有属性。Field成员变量: 描述一个类的属性,内部包含了该属性的所有信息,属性,数据类型等...Constructor构造方法: 描述一个类的构造方法,包含了构造方法的所有信息,参数类型,参数名字等...Method方法对象: 描述一个类的所有方法(包括抽象方法),内部包含了该方法的所有信息,与Constructor类似,不同之处是 Method 拥有返回值类型信息,因为构造方法是没有返回值的
获取Class对象的三种方式
Class.forName("全类名"):将字节码文件加载进内存,返回Class对象
可以用于配置文件,将类名定义在配置文件中。类名.class: 通过类名的属性class获取 可用于参数的传递对象.getClass():getClass()方法在Object类中定义多用于对象的获取字节码的方式
相关信息
同一个字节码文件在一次程序运行的过程中,都只会被记载一次,不论通过哪一种方式获取的Class对象都是同一个
常用的API
- 获取成员变量
- Field[] getFields() :获取所有
public修饰的成员变量 - Field getField(String name) :获取指定名称的
public修饰的成员变量 - Field[] getDeclaredFields() :获取所有的成员变量,不考虑修饰符
- Field getDeclaredField(String name) :根据姓名获取类中的某个变量,
无法获取继承下来的变量
- 获取构造方法
- Constuctor[] getConstructors():获取类中所有被
public修饰的构造器 - Constructor getConstructor(Class...<?> paramTypes):根据
参数类型获取类中某个构造器,该构造器必须被public修饰 - Constructor[] getDeclaredConstructors():获取类中所有构造器
- Constructor getDeclaredConstructor(class...<?> paramTypes):根据
参数类型获取对应的构造器
- 获取方法对象
- Method[] getMethods():获取类中被
public修饰的所有方法 - Method getMethod(String name, Class...<?> paramTypes):根据
名字和参数类型获取对应方法,该方法必须被public修饰 - Method[] getDeclaredMethods():获取所有方法,但
无法获取继承下来的方法 - Method getDeclaredMethod(String name, Class...<?> paramTypes):根据
名字和参数类型获取对应方法,无法获取继承下来的方法
相关信息
每种方法都分为被Declared修饰和没有修饰的方法
被Declared修饰的方法能获取非public的属性,但是无法获取获取继承下来的属性,没有被修饰的方法正相反,所以如果想获取一个对象的所有属性,则需要getXXXs()和getDeclaredXXXs然后使用一个存储不重复数据的容器(如:Set)将两个数据集合并。
/**
* 1.Class.forName
*/
Class clazz = Class.forName("entity.User");
/**
* 2.类名.class
*/
//Class clazz = User.class;
/**
* 对象.getClass()
* User user = new User();
* Class clazz = user.getClass();
*/
Field[] fields1 = clazz.getFields();
Field[] fields2 = clazz.getDeclaredFields();
//Arrays.stream(fields).forEach(System.out::println);
Set<Field> set = new HashSet<Field>();
set.addAll(Arrays.asList(fields1));
set.addAll(Arrays.asList(fields2));
System.out.println("---------------------Field---------------------");
set.stream().forEach(System.out::println);
//Field.get(Object obj) 获取对象的的Field属性值
System.out.println(clazz.getField("Name").get(new User()));
System.out.println(clazz.getField("School").get(new User()));
System.out.println("---------------------methods---------------------");
Method[] methods = clazz.getMethods();
Arrays.stream(methods).forEach(System.out::println);

构造类的实例化对象
获取了Class对象的信息之后,可以以此来实例化一个对象。实例化对象的方式有两种,都是使用newInstance()方法来实现的,newInstance()默认使用无参构造器,constructor.getConstructor(Object... paramTypes) 可以指定使用对应参数的有参构造器。
System.out.println("-----------------实例化对象---------------");
User user = (User) clazz.newInstance();
user.setSchool("家里蹲");
System.out.println(user.getSchool());
Constructor constructor = clazz.getConstructor(String[].class, double.class, double.class);
//禁用访问安全检查
constructor.setAccessible(true);
User user1 = (User) constructor.newInstance(new String[]{"LOL", "ow"},165.00,120.00);
System.out.println(user1.toString());

执行对象的方法
System.out.println("---------------执行对象的方法----------------");
Method method = clazz.getMethod("toString");
System.out.println(method.invoke(user1));
//---------------执行对象的方法----------------
//User{Hobbies=[LOL, ow], Height=165.0, Weight=120.0, School='CQUST', Name='晨曦', Age=0}
反射的使用场景
Spring的IOC容器
在使用Spring的时候通常会使用一个applicationContext.xml的文件,里面保存的bean的信息,使用这些信息来实例化一个bean,然后交给Spring的IOC容器来管理。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="user" class="entity.User">
<constructor-arg type="java.lang.String[]" value="LOL,OW"/>
<constructor-arg type="double" value="165.03"/>
<constructor-arg type="double" value="120.00"/>
</bean>
</beans>
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
User user2 = (User) ac.getBean("user");
System.out.println(user2.toString());
其实使用配置文件来实例化对象过程就是使用的反射,简化的过程如下:接收Class的的名字和路径,然后利用反射来创建一个对象,然后将其放入一个容器中,根据存入的id来在使用的时候将其取出。
String beanId = "user";
String beanPath = "entity.User";
Class<?> cla = Class.forName(beanPath);
User user2 = (User) cla.newInstance();
Map<String,Object> beans = new HashMap<>();
beans.put(beanId,user2);
System.out.println(beans.get("user").toString());
// -------------------------IOC-----------------------
// User{Hobbies=null, Height=0.0, Weight=0.0, School='CQUST', Name='晨曦', Age=0}
反射 + 抽象工厂模式
当我们系统的业务在最开始的使用某一种数据结构来存储数据,但是随着业务变更和系统迭代导致最初的设定不再适用,就需要更改代码,但是如果涉及到的代码过多,并且如果后面也再次变更的话就会大大增加工作量和BUG产生的几率,所以最好是在尽可能减少代码变更的情况下来更改数据存储结构的类型,此时就可以使用反射,每次传入需要使用的数据结构类型的子类,来实例化一个数据结构对象,一劳永逸。同理可以延申到其他地方。
/**
* @param className 类的全限定名
*/
public static Map<Object, Object> produceMap(String className) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Class clazz = Class.forName(className);
Map<Object, Object> map = (Map) clazz.newInstance();
System.out.println("---------produceMap--------");
System.out.println(map.getClass());
return map;
}
//produceMap("java.util.HashMap");
//---------produceMap--------
//class java.util.HashMap
JDBC 加载数据库驱动类
这里数据库按需加载驱动类其实和上面更改数据存储结构一样,都是在减少更改代码的情况下变更原来的需求,我们在application.properties或者application.yml中配置了数据库相关的信息,比如数据源,驱动等等,
spring:
datasource:
username: root
password: 12345
url:
driver-class-name: com.mysql.cj.jdbc.Driver
反射的优缺点
- 优点
- 灵活:面对需求变更时可以灵活的实例化不同的对象
- 缺点
- 安全性:反射能够强制访问被
private修饰的属性 - 性能损耗:反射在使用对象时,不同于直接实例化对象,中间需要经过更过的步骤,而JVM无法对这些步骤进行优化
参考
- Java最强大的技术之一:反射 https://juejin.cn/post/7127479917767294990#heading-7
- 学会反射后,我被录取了 https://juejin.cn/post/6864324335654404104#heading-13
