主要参考HOW2J反射机制系列教程、Java 反射 -超详细讲解(附源码)
一、反射
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
或许有人奇怪,我在编写代码时,也能够获取这个类的所有属性和方法,为何需要反射呢?
我们在编写代码的时候,是在编译前获取对象,而反射是在程序运行时获取对象。
结论: 反射是运行时获取对象用的
应用场景: 之后学习切面的时候,我们需要在运行中获取对象的信息,此时就需要反射,jdk动态代理也用到了反射
二、获取对象
首先,让我们先写一个class
public class Hero {
public String name; //姓名
}
我们获取对象有三个方法(以Hero
举例)
方法 | 说明 |
---|---|
Class.forName(“Hero”) | 通过类名获取class,若包不同则需要添加包名“demo.Hero” |
Hero.class | 直接获取class,需要import |
new Hero().getClass() | 创建对象后获取class |
实际代码如下
public static void main(String[] args) {
String className = "pogo.Hero";
try {
//获取类对象的第一种方式
Class copyClass1 = Class.forName(className);
//获取类对象的第二种方式
Class copyClass2 = Hero.class;
//获取类对象的第三种方式
Class copyClass3 = new Hero().getClass();
System.out.println(copyClass1==copyClass2);//输出true
System.out.println(copyClass2==copyClass3);//输出true
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
发现输出的结果均为true
,这是因为在一个JVM中,一种类,只会有一个类对象存在。所以以上三种方式取出来的类对象,都是一样的,或者说是同一个。
三、使用反射创建对象
-
获取对象:我们需要先获取对象,通过所获取的对象再创建对象,获取对象的方法可参照上文,此处仅只用
Class.forName
的方式获取对象 -
获取构造函数:若要创建对象,则需要使用调用构造函数,获取构造函数的方法有以下几种
方法 | 返回值 | 说明 |
---|---|---|
getConstructors() | Constructor[] | 所有public 构造方法 |
getDeclaredConstructors() | Constructor[] | 获取所有构造方法 |
getConstructor(Class… parameterTypes) | Constructor | 获取指定参数类型public 构造方法 |
getDeclaredConstructor(Class… parameterTypes) | Constructor | 获取指定参数类型的构造方法 |
- 创建对象则是:
Constructor
对象调用newInstance(Object... initargs)
我们修改我们的Hero类如下,添加多个构造方式
public class Hero {
public String name; //姓名
public float hp; //血量
public float armor; //护甲
public int moveSpeed; //移动速度
//(默认的构造方法)
Hero(String name){
System.out.println("(默认)的构造方法 s = " + name);
this.name = name;
}
//无参构造方法
public Hero(){
System.out.println("调用了公有、无参构造方法执行了。。。");
}
//有一个参数的构造方法
public Hero(char name){
System.out.println("姓名:" + name);
this.name = String.valueOf(name );
}
//有多个参数的构造方法
public Hero(String name ,float hp){
System.out.println("姓名:"+name+"血量:"+ hp);
this.name = name;
this.hp = hp;
}
//受保护的构造方法
protected Hero(boolean n){
System.out.println("受保护的构造方法 n = " + n);
}
//私有构造方法
private Hero(float hp){
System.out.println("私有的构造方法 血量:"+ hp);
this.hp = hp;
}
}
主函数如下所示:
public class Main {
public static void main(String[] args) {
try {
// 若是包名不同则添加包名如demo.Hero
Class copyClass = Class.forName("Hero");
System.out.println("————————————————————所有公有构造方法");
Constructor[] conArray = copyClass.getConstructors();
for (Constructor constructor: conArray) {
System.out.println(constructor);
}
System.out.println("————————————————————所有构造方法");
Constructor[] conArray2 = copyClass.getDeclaredConstructors();
for (Constructor constructor: conArray2) {
System.out.println(constructor);
}
System.out.println("————————————————————获取共有,参数为null(即无参)的构造方法");
Constructor conSingle = copyClass.getConstructor(null);
System.out.println(conSingle);
Object object = conSingle.newInstance();
System.out.println(object);
System.out.println("————————————————————获取参数为float类型的构造方法");
Constructor conSingle2 = copyClass.getDeclaredConstructor(float.class);
System.out.println(conSingle2);
System.out.println("强行访问私有类");
conSingle2.setAccessible(true);
Object object2 = conSingle2.newInstance(100);
System.out.println(object2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
若觉得catch太多,可直接使用即可
catch (Exception e) {
e.printStackTrace();
}
输出结果如下
————————————————————所有公有构造方法
public Hero(java.lang.String,float)
public Hero()
public Hero(char)
————————————————————所有构造方法
private Hero(float)
protected Hero(boolean)
public Hero(java.lang.String,float)
Hero(java.lang.String)
public Hero()
public Hero(char)
————————————————————获取共有,参数为null(即无参)的构造方法
public Hero()
调用了公有、无参构造方法执行了。。。
Hero(name=null, hp=0.0, armor=0.0, moveSpeed=0)
————————————————————获取参数为float类型的构造方法
private Hero(float)
强行访问私有类
私有的构造方法 血量:100.0
Hero(name=null, hp=100.0, armor=0.0, moveSpeed=0)
三、获取对象的属性并修改
步骤:
- 获取对象
- 获取成员变量
- 修改成员变量
获取成员变量的方式有两种
方法 | 说明 |
---|---|
getField | 获取公有属性 |
getDeclaredField | 获取属性(不能获取继承来的字段) |
这里所说的不能获取继承来的字段指的是:只能获取到private的字段,但并不能访问该private字段的值,除非加上setAccessible(true)
且需要注意的事情是,getDeclaredField
虽然可以获取私有属性,但是不能对其进行修改,除非加上setAccessible(true)
我们继续修改Hero类,添加get、set方法,去掉过多的构造方法
public class Hero {
private String name; //姓名
private float hp; //血量
private float armor; //护甲
public int moveSpeed; //移动速度
//无参的构造方法
public Hero(){
System.out.println("无参的构造方法");
}
public Hero(String name){
System.out.println("一个参数的构造方法 name = " + name);
this.name = name;
}
//有多个参数的构造方法
public Hero(String name ,float hp){
System.out.println("姓名:"+name+"血量:"+ hp);
this.name = name;
this.hp = hp;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getHp() {
return hp;
}
public void setHp(float hp) {
this.hp = hp;
}
public float getArmor() {
return armor;
}
public void setArmor(float armor) {
this.armor = armor;
}
public int getMoveSpeed() {
return moveSpeed;
}
public void setMoveSpeed(int moveSpeed) {
this.moveSpeed = moveSpeed;
}
}
主函数如下所示:
public static void main(String[] args) {
// 新建对象
Hero hero = new Hero();
// 设置name
hero.setName("盖伦");
try {
Field field = hero.getClass().getDeclaredField("name");
// 由于name 是私有属性,所以需要setAccessible(true);才可以进行修改
field.setAccessible(true);
field.set(hero, "提莫");
System.out.println(hero.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
执行结果如下:
无参的构造方法
提莫
四、获取并使用对象的方法
步骤:
- 获取对象
- 获取对象方法
- 调用方法
获取对象方法 的方法如下所示:
方法 | 返回值 | 说明 |
---|---|---|
getMethod(String name ,Class<?>… parameterTypes) | Method | 获取公有方法 |
getDeclaredMethods(String name ,Class<?>… parameterTypes) | Method | 获取所有方法(不包括继承) |
调用方法如下所示:
method.invoke(Object obj,Object… args)
obj
: 要调用方法的对象;
args
:调用方式时所传递的实参;
代码示例:
public static void main(String[] args) {
// 新建对象
Hero hero = new Hero();
// 设置name
hero.setName("盖伦");
try {
Method method = hero.getClass().getMethod("setName", String.class);
method.invoke(hero,"提莫");
System.out.println(hero.getName());//提莫
} catch (Exception e) {
e.printStackTrace();
}
}
五、获取并使用对象的main方法
我们修改Hero,在其中添加一个main方法
public class Hero {
public static void main(String[] args) {
System.out.println("执行main方法");
for (String str : args) {
System.out.println(str);
}
}
}
获取main的方式,与四、获取并使用对象的方法
类似,只是main的参数需要注意,这边我就直接上代码了
public static void main(String[] args) {
try {
Class copyClass = Class.forName("Hero");
Method method = copyClass.getMethod("main",String[].class);
method.invoke(null,(Object) new String[]{"a","b"});
// method.invoke(null, new Object[]{new String[]{"a","b"}});//方式二
}catch (Exception e) {
e.printStackTrace();
}
}
六、反射的应用
反射在实际工程中有着很大的作用,理解反射是理解依赖注入、控制反转、切面的基础。
此处先用两个简单例子。
六、1 利用反射,通过修改配置文件切换服务
1、准备两个Service
package cn.demo.module.learn;
public class ServiceOne {
public void doServiceOne() {
System.out.println("执行业务1");
}
}
package cn.demo.module.learn;
public class ServiceTwo {
public void doServiceTwo() {
System.out.println("执行业务2");
}
}
准备好我们的配置文件springService.txt
class=cn.demo.module.learn.ServiceTwo
method=doServiceTwo
测试用函数如下所示:
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(3000);
//springService.txt中获取类名称和方法名称
File springConfigFile = new File("D:\\springService.txt");
Properties springConfig= new Properties();
springConfig.load(new FileInputStream(springConfigFile));
String className = (String) springConfig.get("class");
String methodName = (String) springConfig.get("method");
// 根据类名称获取类对象
Class copyClass = Class.forName(className);
// 根据名称,获取方法对象
Method method = copyClass.getMethod(methodName);
// 获取构造器
Constructor constructor = copyClass.getConstructor();
// 通过构造器生成对象
Object service = copyClass.newInstance();
// 调用指定的方法
method.invoke(service);
} catch (Exception e) {
e.printStackTrace();
}
}
}
开始运行后,我们在中途修改springService.txt
,改成如下所示内容:
class=cn.demo.module.learn.ServiceOne
method=doServiceOne
执行结果如下所示:
可发现,我们并没有修改代码,但是成功从执行serviceTwo切换为serviceOne
六、2 通过反射,绕过泛型
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("this");
list.add("is");
// list.add(5);报错
try {
/********** 越过泛型检查 **************/
//获取ArrayList的Class对象,反向的调用add()方法,添加数据
Class listClass = list.getClass();
//获取add()方法
Method m = listClass.getMethod("add", Object.class);
//调用add()方法
m.invoke(list, 5);
} catch (Exception e) {
e.printStackTrace();
}
// 此处list.add(5);仍报错,即泛型并没有被修改
//遍历集合
for (Object obj : list) {
System.out.println(obj + " is " + obj.getClass().toString());
}
}
结果如下所示:
this is class java.lang.String
is is class java.lang.String
5 is class java.lang.Integer
可见仅有5是Integer
类型,在后续想要执行list.add(5)
报错,是因为泛型并没有被修改,仍是String
类型,所以是越过泛型检查。
这是因为 泛型是在编译期间起作用的。在编译后的.class文件中是没有泛型的。所有比如T或者E类型啊,本质都是通过Object处理的。
所以可以通过使用反射来越过泛型。