对于Java初学者来说,反射是一个非常不好理解的概念。如果没有一定的编程经验的积累,是很难对Java反射有一个很好的理解。在一般开发中,很少会使用反射。但是,如果您要开发框架,或者写一些工具的时候,就很有可能需要使用到反射。
举个例子,现在我有一个文件,文件内容如下所示:
className=com.xjy.demo.Person
name=lily
age=20
上面内容包含了一个className(类名)、name(姓名)、age(年龄)的信息。如果要求写一个Java程序,读取该文件的内容,并且根据这些信息创建对应的Java对象。如何实现???
我相信只要大家学过Java基础的IO编程,一定知道如何读取一个文件的内容。 但是,读取到内容后,如何根据读取到的内容信息创建对应的Java对象,并且对对象中的属性进行初始化呢?
其实,Spring框架就是这样做的。它也是把类的信息定义在一个文件中。Spring容器负责读取文件中的类的信息,然后通过反射技术创建对应的Java对象。
所以,如果要实现以上功能,就需要使用反射的知识。
在学习反射之前,大家先要了解一下Java类在JVM中是如何被调用。比如下面这段代码:
package com.xjy.demo;
public class Person {
public String name;
public int age;
String gender;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void work() {
System.out.println("工作...");
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", gender=" + gender
+ "]";
}
}
你们知道上面这段代码在JVM中是如何执行吗?下面我们一起来分析一下。
1)当程序运行的时候,jvm首先会把Demo01的字节码文件加载到内存中,然后再对该字节码文件进行解剖,把该类的成员信息解剖出来。解剖完成后,JVM就会创建一个Class对象,然后把解剖出来的信息保存在该对象中。
2)当调用main方法的时候,上面main方法里面创建了一个Person对象。所以,jvm也会把该Person的字节码文件也加载到内存中。同样地,加载完后也会对它进行解剖然后把里面的name、age等成员信息解剖出来,然后创建再创建一个Class对象,并把解剖出来的信息保存在Class对象中。
从上面分析可以得到一个结论:一个类的信息(类名、属性、方法等等)都是保存在一个Class对象中。因此,如果要获取一个类的这些信息,就可以通过它的Class对象来获取。Class类提供了大量方法来操作一个类的成员,后面我们会给大家详细介绍。
其实,我们学习反射,就是要学习如何获取一个类的Class对象,如何调用Class对象提供的方法去操作类的成员。只要大家了解这个之后,就已经达到了基本目的。至于更加高级的用法,需要大家以后在工作中慢慢体会和进一步学习。
上面笔者也介绍过,使用反射其实就是要找到一个类的Class对象,然后在通过这个Class对象的方法来操作类的成员。所以,获取Class对象是学习反射的第一步。
获取一个类的Class对象有三种方式:
(1)通过Class类的静态方法forName来获取。
public class Demo01 {
public static void main(String[] args) throws ClassNotFoundException {
Class clz_1 = Class.forName("com.xjy.demo.Person");
System.out.println(clz_1);
}
}
(2)通过“类名.class”直接获取。
public class Demo01 {
public static void main(String[] args) throws ClassNotFoundException {
Class clz_1 = Class.forName("com.xjy.demo.Person");
Class clz_2 = Person.class;
System.out.println(clz_1 == clz_2);
}
}
运行上面程序,控制台输出true,表示clz_1和clz_2是同一个对象。
(3)通过对象的getClass()来获取。
public class Demo01 {
public static void main(String[] args) throws ClassNotFoundException {
Class clz_1 = Class.forName("com.xjy.demo.Person");
Class clz_2 = Person.class;
System.out.println(clz_1 == clz_2);
Person p = new Person();
Class clz_3 = p.getClass();
System.out.println(clz_2 == clz_3);
}
}
运行上面程序发现,clz3和clz2也是同一个对象。这样推断出clz1和clz3也是相等的。
所以,从测试代码可以看出,这三种方式获取到的Class对象是同一个对象。
(1)获取构造函数
public class Demo02 {
public static void main(String[] args) throws Exception {
// 创建Class对象
Class clazz = Class.forName("com.xjy.demo.Person");
// 根据参数类型获取公共的构造函数
Constructor c = clazz.getConstructor(String.class, int.class);
System.out.println(c);
// 获取包含所有公共构造方法的数组
Constructor[] constructors = clazz.getConstructors();
for (Constructor c1 : constructors) {
System.out.println(c1);
}
}
}
获取Constructor代表构造函数。有了Constructor后,就可以调用它的newInstance方法创建实例。
// 创建Person实例
Constructor c = clazz.getConstructor();
Object o = c.newInstance();
(2)获取字段
public class Demo03 {
public static void main(String[] args) throws Exception {
// 创建Class对象
Class clazz = Class.forName("com.xjy.demo.Person");
// 获取公共字段
Field nameField = clazz.getField("name");
System.out.println(nameField);
// 获取所有公共字段
Field[] fields = clazz.getFields();
for (Field field : fields) {
System.out.println(field);
}
}
}
上面两个方法只能够获取public修饰的字段,如果要获取非公共字段,那么就要使用下面方法:
// 获取公共字段
Field nameField = clazz.getDeclaredField("name");
System.out.println(nameField);
// 获取所有公共字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field);
}
上面代码中的Field代表字段。如果要操作字段,那么就要使用Field对象提供的方法。Field对象提供了一些setXxx和getXxx方法用于设置和获取字段的值。例如:
// 创建Person实例
Constructor c = clazz.getConstructor();
Object o = c.newInstance();
// 获取公共字段
Field nameField = clazz.getDeclaredField("name");
System.out.println(nameField);
// 设置字段的值
nameField.set(o, "lily");
System.out.println(o);
因为静态成员是不需要对象调用。所以,如果上面程序中的name字段是静态,那么调用setXxx方法的时候,第一个参数可以设置为null。还有,如果name字段是私有的,那么在执行setXxx方法的时候,需要先执行下面代码,否则setXxx执行失败!!!
nameField.setAccessible(true);
(3)获取方法
public class Demo04 {
public static void main(String[] args) throws Exception {
// 创建Class对象
Class clazz = Class.forName("com.xjy.demo.Person");
// 获取方法
Method method = clazz.getMethod("toString");
System.out.println(method);
// 获取所有方法,包含父类的方法
Method[] methods = clazz.getMethods();
for (Method method2 : methods) {
System.out.println(method2);
}
}
}
上面两个方法只能够获取public修饰的方法,获取非公共方法的代码如下所示:
// 获取指定方法,包含公共和非公共的
Method method = clazz.getDeclaredMethod("walk");
System.out.println(method);
// 获取本类的所有方法,包含公共和公共的
Method[] methods = clazz.getDeclaredMethods();
for (Method method2 : methods) {
System.out.println(method2);
}
Method代表方法。如果要执行方法,那么可以调用Method对象的invoke方法去执行。
// 创建Class对象
Class clazz = Class.forName("com.xjy.demo.Person");
// 获取方法
Method method = clazz.getMethod("work");
// 创建Person实例
Constructor c = clazz.getConstructor();
Object o = c.newInstance();
// 调用方法
method.invoke(o);
同样的,如果work方法是静态的,那么在调用invoke方法的时候,第一个参数可以为null。
另外,如果方法使用private修饰,那么在调用invoke方法之前,需要先执行下面代码,否则执行失败!!!
method.setAccessible(true);
到目前为止,我们大概了解Java反射的基本用法。那么下面我们以一个小案例把反射的知识作一个总结。
在文本最开始的时候,我们有这样一个使用场景:有一个person.txt文件,里面保存了一个类的信息。现在要求我们根据这些信息创建该类的实例。
className=com.xjy.demo.Person
name=lily
age=20
具体的实现步骤:
(1)读取文件内容
public class Demo05 {
public static void main(String[] args) throws Exception {
// 创建缓冲输入字符流对象
BufferedReader br = new BufferedReader(new FileReader("person.txt"));
// 读取第一行
String line1 = br.readLine();
// 获取类名
String className = line1.split("=")[1];
// 读取第二行
String line2 = br.readLine();
// 获取name
String name = line2.split("=")[1];
// 读取第三行
String line3 = br.readLine();
// 获取age
String age = line3.split("=")[1];
System.out.println("className : " + className);
System.out.println("name : " + name);
System.out.println("age : " + age);
// 关闭流
br.close();
}
}
运行上面程序,在控制台上可以看到如下输出:
(2)读取到文件内容后,接下来我们就要使用反射的知识创建Person实例,并且为Person实例的每一个字段赋值。
// 创建缓冲输入字符流对象
BufferedReader br = new BufferedReader(new FileReader("person.txt"));
// 读取第一行
String line1 = br.readLine();
// 获取类名
String className = line1.split("=")[1];
// 通过类名创建实例
Class personClass = Class.forName(className);
Object o = personClass.newInstance();
// 读取第二行
String line2 = br.readLine();
// 获取name
String name = line2.split("=")[1];
// 设置name字段的值
Field nameField = personClass.getField("name");
nameField.set(o, name);
// 读取第三行
String line3 = br.readLine();
// 获取age
String age = line3.split("=")[1];
// 设置age字段的值
Field ageField = personClass.getField("age");
ageField.set(o, Integer.parseInt(age));
System.out.println(o);
// 关闭流
br.close();
运行程序,控制台输入如下:
Java8在java.lang.reflect包下新增了一个Executable抽象基类,该类派生出Constructor和Method两个子类。Excutable为Constructor和Method扩展了一些公共方法:
方法名 | 作用 |
int getParameterCount() | 获取构造函数或方法的参数个数 |
Parameter[] getParameters() | 获取构造函数或方法的所有形参 |
Parameter也是Java8新增的类,每个Parameter对象代表构造函数或方法的形参。Parameter也提供了一些方法来获取参数信息。
方法名 | 作用 |
getModifiers() | 获取形参的修饰符 |
String getName() | 获取形参的名字 |
Type getParameterizedType() | 获取带泛型的形参类型 |
Class> getType() | 获取形参类型 |
boolean isNamePresent() | 判断class文件中是否包含方法形参名的信息 |
boolean isVarArgs() | 判断该参数是否是个数可变的形参 |
默认情况,执行javac命令生成的class文件是不包含形参名的信息。所以,如果要让class文件包含形参名的信息,那么在执行javac命令的时候,指定-parameters选项。例如:
>> javac -parameters -d . Demo03.java
示例代码:
class Test {
public void test(String s, List list) {
}
}
public class Demo99 {
public static void main(String[] args) throws Exception {
Class clazz = Test.class;
Method testMethod = clazz.getMethod("test", String.class, List.class);
System.out.println("parameter count :" + testMethod.getParameterCount());
Parameter[] parameters = testMethod.getParameters();
for (Parameter parameter : parameters) {
if (parameter.isNamePresent()) {
System.out.println("getName() : " + parameter.getName());
System.out.println("getType() : " + parameter.getType());
System.out.println("getParameterizedType() : " + parameter.getParameterizedType());
}
}
}
}
至此,Java反射先总结到这里,谢谢!!!