JAVA反射概念及使用详解
一、什么是反射?
反射:框架设计的灵魂
框架:半成品软件。可以在框架的基础上进行软件开发,简化编码
反射:将类的各个组成部分封装为其他对象,这就是反射机制
好处:
可以在程序运行过程中,操作这些对象。
可以解耦,提高程序的可扩展性。
定义:JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,这种动态获取、调用对象方法的功能称为java语言的反射机制。
反射是通过Class对象(字节码文件),来知道某个类的所有属性和方法。也就是说通过反射我们可以获取构造器,对象,属性,方法(原本不知道)
不像现在这个类我们能看见,之后在JAVA框架中,很多类我们是看不见的,不能直接用类名去获取对象,只能通过反射去获取。
二、获取Class对象的三种方式:
要想使用反射,必须先得到代表的字节码的Class对象,Class类用于表示.class文件(字节码)
1.通过该类的对象去获取到对应的Class对象(基本不用它)
//第一种方式: student--->Class对象 通过getClass()方法
//Student是一个空类
Student student = new Student();
//这里我们就省去泛型了,Class<?>
Class stuClass = student.getClass(); //获取到了对应的Class对象
System.out.println(stuClass);
System.out.println(stuClass.getName()); //获取Class对象的名字
输出:
class fanshe.Student
fanshe.Student
但是需要注意的是,第一种我们基本不用,这里显然和反射机制相悖(你有类对象 student 还去用反射获取Class类对象干嘛,多此一举)
2.通过类名.class静态属性获取(比较简单)
//第二种方式: 每个类创建后 都会有一个默认的静态的class属性 用于返回该类的class对象
//需要注意的是: 任何数据类型(包括基本数据类型)都有“静态”的class属性
Class stuClass2 = Student.class;
System.out.println("是否为同一个class对象?"+(stuClass==stuClass2));
结果:true
这里需要注意的是,这种方式虽然比较简单,但是需要导包,不然会编译错误(对比第三种,全限定类名方式)
3.通过Class类中的静态方法 forName()方法获取(最为常见)
//第三种方式: Class.forName("fanshe.Student"); 注意参数一定为该类的全限定类名
try {
Class stuClass3 = Class.forName("fanshe.Student");
//System.out.println(stuClass3); 输出仍然是class fanshe.Student
System.out.println("是否为同一个class对象?"+(stuClass3==stuClass2));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
结果:true
结论:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。
二、通过Class对象获取到该类的构造器:
获取到Class对象后,我们就可以通过Class对象获取到该类的构造器了。有很多方法,以下逐一介绍:
首先创建一个Class对象
Class stuClass = Student.class;//我们采用了第二种方式
这个是我的Student类,为了演示构造了多个构造器
public class Student {
//通过多个构造器不同的修饰符 不同的形参列表
Student(String name) {
System.out.println("用default修饰的Student的含有一个String参数的构造器:"+name);
}
public Student() {
System.out.println("用public修饰的Student的无参构造器");
}
public Student(String name,int age) {
System.out.println("用public修饰的Student的含有两个参数的构造器:"+name+age);
}
public Student(boolean sex) {
System.out.println("用public修饰的Student的含有一个参数的构造器:"+sex);
}
protected Student(int age) {
System.out.println("用protected修饰的Student的含有一个参数的构造器:"+age);
}
private Student(String name,int age,boolean sex) {
System.out.println("用private修饰的Student的含有三个参数的构造器:"+name+age+sex);
}
}
1.getDeclaredConstructors()
返回 Constructor 对象的一个数组,这些对象反映此 Class 对象表示的类声明的所有构造方法
Constructor[] conArray01 = stuClass.getDeclaredConstructors();
for (Constructor constructor : conArray01) {
System.out.println(constructor);
}
//输出结果:输出了所有的构造器
private fanshe.Student(java.lang.String,int,boolean)
protected fanshe.Student(int)
public fanshe.Student(boolean)
public fanshe.Student(java.lang.String,int)
public fanshe.Student()
fanshe.Student(java.lang.String)
2.getConstructors()
返回一个包含某些 Constructor 对象的数组,这些对象反映此 Class 对象所表示的类的所有公共(public)构造方法。
Constructor[] consArray02 = stuClass.getConstructors();
for (Constructor constructor : consArray02) {
System.out.println(constructor);
}
//输出的是所有的public修饰的构造方法:
//public fanshe.Student(boolean)
//public fanshe.Student(java.lang.String,int)
//public fanshe.Student()
3.getConstructor(Class<?>... parameterTypes)
返回一个 Constructor 对象,它反映此 Class 对象所表示的类的指定公共(public)构造方法。
//参数为长度可变的形参:获取指定的构造器 参数为指定构造器的形参类型对应的class对象
//用public修饰的构造器
Constructor con0 = stuClass.getConstructor(null); //无参的 无参构造器参数就是null或者空,等同于stuClass.getConstructor()
Constructor con = stuClass.getConstructor(boolean.class); //boolean的
System.out.println(con0);
System.out.println(con);
//输出:
//public fanshe.Student()
//public fanshe.Student(boolean)
注意,输出的都是public类型的,也只能是public类型的。
4.getDeclaredConstructor(Class<?>... parameterTypes)
返回一个 Constructor 对象,该对象反映此 Class 对象所表示的类或接口的指定构造方法。
Constructor con1 = stuClass.getDeclaredConstructor(int.class);
System.out.println(con1);
Constructor con2 = stuClass.getDeclaredConstructor(String.class,int.class,boolean.class);
System.out.println(con2);
//输出:
//protected fanshe.Student(int)
//private fanshe.Student(java.lang.String,int,boolean)
总结:
①有参数的3和4,返回的都是一个构造器(好理解,构造器重载要求参数列表不一样)。没参数的1和2,返回的都是Constructor类型的数组。你可以这样记:方法名加了s,例getConstructors(), 的返回都是多个构造器,没有s的都是一个。
②方法名包含了Declared返回的构造器访问权限修饰符没有限制,而没有包含Declared例getConstructors(),返回的都是public修饰的构造器。
简而言之:加s的比没加s的返回的构造方法多,加Declared比没加的多,全加的(getDeclaredConstructors())最多
那么获取了构造器如何去创建对象呢?
三、通过获取到的构造器创建对象:
//这里我们需要捕获一下异常
//因为我们的构造器是私有的,不能在其他类去创建,所以我们用了setAccessible()这个方法,从而可以创建
/*
private Student(String name,int age,boolean sex){
System.out.println("用private修饰的Student的含有三个参数的构造器:"+name+age+sex);
}*/
try {
con2.setAccessible(true); //忽略构造器的访问修饰符,解除私有限定
Object object = con2.newInstance("张三",10,true); //这句话就相当于new Student("张三",10,true);
Student student = (Student) object; //指向子类对象的父类引用重新转为子类引用
} catch (Exception e) {
e.printStackTrace();
}
//输出:用private修饰的Student的含有三个参数的构造器:张三10true
注意:
xxx.setAccessible(true) 是为了解除私有限定,以后会经常出现,之后就不多加解释了
四、 通过Class对象获取成员变量
以下方法有很多异常需要抛出,我就省略了
先定义一个Teacher类:
public class Teacher {
public Teacher() {
}
public String name;
protected int age;
boolean sex;
private String address;
@Override
public String toString() {
return "Teacher [name=" + name + ", age=" + age + ", sex=" + sex + ", address=" + address + "]";
}
}
Class teaClass = Class.forName("fanshe.Teacher"); //创建Class对象(第三种方式)
//获取Teacher类中的所有的public字段
Field[] f = teaClass.getFields();
for (Field field : f) {
System.out.println(field);
}
//获取Teacher类中的所有的字段(包含各种访问修饰符)
System.out.println("===========所有的字段=======================>");
Field[] df = teaClass.getDeclaredFields();
for (Field field : df) {
System.out.println(field);
}
System.out.println("===========公共的特定的字段=======================>");
// 获取公共的特定的字段
Field field = teaClass.getField("name");
System.out.println(field);
System.out.println("===========特定的字段=======================>");
// 获取特定的字段
Field field2 = teaClass.getDeclaredField("age");
System.out.println(field2);
/**
* 为字段设置具体的值
* 参数一:该类的对象
* 参数二:为特定的属性赋值
*/
Object object = teaClass.getConstructor().newInstance();//相当于Object object = new Teacher();
field.set(object, "张三丰");
System.out.println("名称为:"+((Teacher)object).name);
输出结果:
public java.lang.String fanshe.Teacher.name //所有public字段
===========所有的字段=======================>
public java.lang.String fanshe.Teacher.name
protected int fanshe.Teacher.age
boolean fanshe.Teacher.sex
private java.lang.String fanshe.Teacher.address
===========公共的特定的字段=======================>
public java.lang.String fanshe.Teacher.name
===========特定的字段=======================>
protected int fanshe.Teacher.age
名称为:张三丰
记忆方法同上,这里就不赘述了。
五、通过Class对象获取到该类的方法
首先创建了一个Employee类:
public class Employee {
public Employee() {}
public String name;
protected double money;
String address;
private long number;
protected void getDayMoney(String name) {
System.out.println("我是Employee受保护的获取日薪的方法,有一个参数为:"+name);
}
public void getweekMoney(String name,double money) {
System.out.println("我是Employee公有的获取周薪的方法,没有参数..");
}
void getMonthMoney() {
System.out.println("我是Employee默认的获取月薪的方法,没有参数..");
}
private void getYearMoney(int age) {
System.out.println("我是Employee私有的的获年薪月薪的方法,有一个参数为:"+age);
}
public static void main(String[] args) {
System.out.println("Employee中的main()方法执行了...");
System.out.println(Arrays.toString(args));
}
@Override
public String toString() {
return "Employee [name=" + name + ", money=" + money + ", address=" + address + ", number=" + number + "]";
}
}
通过Class对象去获取该类的方法:
Class<?> emplClass = Class.forName("fanshe.Employee");
//1.获取所有的公共的方法 (包含父类的方法)
Method[] methods = emplClass.getMethods();
for (Method method : methods) {
System.out.println(method);
}
//输出:
public java.lang.String fanshe.Employee.toString()
public void fanshe.Employee.getweekMoney(java.lang.String,double)
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
注意:
1.上面的方法都是public公共的;
2.会将父类(Object)类中的公共方法也输出。
//2.获取该类中的所有方法,以数组形式返回:
Method[] methods = emplClass.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method);
}
//输出:
public java.lang.String fanshe.Employee.toString()
public void fanshe.Employee.getweekMoney(java.lang.String,double)
private void fanshe.Employee.getYearMoney(int)
void fanshe.Employee.getMonthMoney()
protected void fanshe.Employee.getDayMoney(java.lang.String)
//3.获取特定的公有的方法: 只能是公共的
//参数一:要获取的方法的名称,参数二:方法对应的形参列表的类型
Method method = emplClass.getMethod("getweekMoney",new Class[]{String.class,double.class});
System.out.println(method); //输出:public void fanshe.Employee.getweekMoney(java.lang.String,double)
//4.获取特定的方法: 可以是私有的
Method method2 = emplClass.getDeclaredMethod("getYearMoney", int.class);
System.out.println(method2); //输出:private void fanshe.Employee.getYearMoney(int)
接下来我准备获取private void getYearMoney(int age) {}方法,然后去执行这个私有方法。
//emplClass对应的类的对象
Object obj = emplClass.getConstructor().newInstance();
//可以通过Employee类对象去直接调用非私有方法,我这里将测试类和Employee类放在了同一个包下,所有可以访问默认方法(包限定方法)
Employee employee = (Employee)obj;
employee.getMonthMoney();
//参数一:要调用的对象 参数二:方法具体要求传递的值 返回值:为调用该方法后的返回值所对应的对象,如果没有返回值(void),则对象为null
method2.setAccessible(true);
Object result = method2.invoke(obj, 20); //执行method2方法对应的代码
System.out.println(result);
//输出:
/*
我是Employee默认的获取月薪的方法,没有参数..
我是Employee私有的的获年薪月薪的方法,有一个参数为:20
null
*/
调用Employee类的主方法(main):
Class<?> class1 = Class.forName("fanshe.Employee");
Method methodMain = class1.getMethod("main", String[].class);
//参数一:对象类型 null 当调用的方法为静态时,此时第一个参数可以为null
//方式一:
Object object = methodMain.invoke(null, (Object)new String[]{"a","b","c","d"});
//方式二:
Object object = methodMain.invoke(null, new Object[] {new String[]{"a","b","c","d"}});
System.out.println(object);
//输出:
Employee中的main()方法执行了... //这里是main方法里的输出
[a, b, c, d] //main方法里的输出调用了toString
Employee中的main()方法执行了...
[a, b, c, d]
null //main方法返回类型为void,所以这里返回值为null
六、通过Method对象调用指定方法
下面我们对Method中的invoke()方法进行详解。
public Object invoke(Object obj, Object... args)
对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。
如果底层方法是静态的,那么可以忽略指定的 obj 参数。该参数可以为 null。
如果底层方法所需的形参数为 0,则所提供的 args 数组长度可以为 0 或 null。
如果底层方法是静态的,并且尚未初始化声明此方法的类,则会将其初始化。
如果方法正常完成,则将该方法返回的值返回给调用者;如果该值为基本类型,则首先适当地将其包装在对象中。但是,如果该值的类型为一组基本类型,则数组元素不 被包装在对象中;换句话说,将返回基本类型的数组。如果底层方法返回类型为 void,则该调用返回 null。
参数:
obj - 从中调用底层方法的对象
args - 用于方法调用的参数
返回:
使用参数 args 在 obj 上指派该对象所表示方法的结果
抛出:抛出的异常就不列举了,具体可以自己查API文档
下面我单独创建了一个InvokeTest类去解释说明invoke方法的使用:
public class InvokeTest {
public static void main(String[] args) throws Exception {
Class classType = InvokeTest.class;
Object invoketest = classType.getConstructor().newInstance();
//获取Method类对象
Method m = classType.getMethod("add", new Class[] {int.class,int.class});
/**
* Method的invoke(Object obj,Object[] args) 该方法接收的参数必须为对象
* 如果参数为基本数据类型,使用相应的包装类对象 返回的结果总是一个对象(其结果代表调用该方法后的返回值)
*/
//如果add方法是静态方法,那么可以这样写:Object object = m.invoke(null, new Integer(10), new Integer(20));
Object object = m.invoke(invoketest, new Integer(10), new Integer(20));
System.out.println(object);
}
public int add(int param1,int param2) {
return param1+param2;
}
}
//输出:30
七、反射的使用
1.通过反射运行配置文件内容:
通过这种方式,当我们需要访问其他类的时候,不需要改动源码,利用反射,直接修改配置文件即可。
public class FileDemo {
public static void main(String[] args) throws Exception {
Class aClass = Class.forName(getValue("className"));
Method m = aClass.getMethod(getValue("methodName"));
m.invoke(aClass.getConstructor().newInstance());
}
//这个方法用于接收配置文件中与key所对应的value值
public static String getValue(String key) throws Exception {
Properties pro = new Properties(); //获取配置文件的对象
FileReader reader = new FileReader("prop.txt");
pro.load(reader); //将流加载到配置文件对象中
//从输入流中读取属性列表(键和元素对)。通过对指定的文件(比如说上面的 prop.txt 文件)进行装载来获取该文件中的所有键 - 值对。以供 getProperty ( String key) 来搜索。
reader.close();
return pro.getProperty(key);//用指定的键在此属性列表中搜索属性。也就是通过参数 key ,得到 key 所对应的 value。
}
}
//prop.txt文件中的内容
className=fanshe.file.Apple
methodName=taste
//Apple类
public class Apple {
public void taste() {
System.out.println("苹果真好吃啊...");
}
}
//输出:
苹果真好吃啊...
总结一下步骤:
1.通过Class类中的forName静态方法获取Apple类的Class类对象;
2.通过Class类对象获取Method类对象,参数传入要调用方法的方法名;
3.通过Method类的invoke非静态方法,调用这个方法,参数传入类对象和调用的方法的参数对象(我这里的test方法没有参数,所有invoke参数只有Apple类的对象)。
Java中有个比较重要的类Properties(Java.util.Properties),主要用于读取Java的配置文件。
上面的例子是获取text文件中的,而配置文件(.properties文件)读取方式略有不同:
public class ReflectTest {
public static void main(String[] args) throws Exception {
//1.加载配置文件
//1.1创建Properties对象
Properties pro = new Properties();
//1.2获取到class目录下的配置文件
ClassLoader classLoader = ReflectTest.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream("pro.properties");
//1.3加载配置文件
pro.load(is);
//2.获取到配置文件中定义的数据
String className = pro.getProperty("className");
String methodName = pro.getProperty("methodName");
//3.有了全限定类名 有了方法名(参数) 完全可以通过反射调用该方法
//3.1:获取到该类的Class对象
Class cls = Class.forName(className);
//3.2:获取到该类的实例对象
Object object = cls.getConstructor().newInstance();
//3.3:获取到该类特定的方法对象
Method method = cls.getMethod(methodName);
//4.执行方法
method.invoke(object);
}
}
//pro.properties文件内容,这个配置文件可以包->右键->New->File->File name:pro.properties
className=bean.Student
methodName=study
Student类和运行结果这里就不列举了,和上面的差不多
2.通过反射越过泛型检查:
我们的目的是:向String泛型集合中添加一个Integer对象
List<String> list = new ArrayList<>();
list.add("我爱Java");
list.add("成功上岸");
//list.add(100); 如果直接add直接会编译报错
//1.获取到List类的Class对象
Class listClass = list.getClass();
Method method = listClass.getMethod("add", Object.class);
method.invoke(list, 100);
for (Object object : list) {
System.out.println(object);
}
/*输出:
我爱Java
成功上岸
100
*/
如有错误,欢迎指点