我对Java反射的理解

一、反射介绍

对于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对象是学习反射的第一步。

获取一个类的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对象是同一个对象。

请记住:在JVM里面,一个Java类就只有一个Class对象。

 

四、从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反射新增特性

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<String> 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反射先总结到这里,谢谢!!!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值