谈到反射,可能我们会觉得很陌生,因为平时的开发似乎根本没有用到这个概念。但结果恰恰相反,反射是与我们日常开发关联最密切的东西。
举个例子,在当我们使用eclipse写代码的时候,在任意一个对象的后面写一个点就会罗列出来该对象的所有属性和方法,这其实就是使用反射实现的。再比如使用框架(如SpringBoot)的时候需要写一些配置文件,这些配置文件也是通过反射注入到你的代码中的。例如这样:
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:8080/testdb?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123
所以反射最重要的用途就是开发各种框架。那么说了这么多,反射到底是什么呢?
我们先来看一段很常见的代码:
Person p = new Person();
p.setName("张三");
很明显,JVM对这段代码进行编译的时候,已经知道了要使用的类(Person),并且要用到Person类的setName()方法。
而反射恰恰相反——在编译期JVM不知道具体要使用什么类以及要用到这个类的什么方法、什么属性。换而言之,反射是JVM在运行期动态的加载类或者调用类的方法和属性(注意这里强调的是运行期)。
获取Class对象
要想运用反射就离不开Class对象。在不同的阶段,获取Class对象有不同的方式。
1、源码阶段:Person.java
获取方式:Class.forName("org.hu.test.entity.Person")
作用:可以用来读取配置文件,例如加载数据库驱动
2、字节码阶段:Peson.class
获取方式:Person.class
作用:可以用作同步监视器(在这篇文章中有提及:Java基础(7) 多线程)
3、对象阶段:Person p = new Person()
获取方式:p.getClass()
作用:可以用来判断是否是同一个字节码文件,比较典型的是equals()方法
具体可以参考下面的例子:
package org.hu.test.entity;
public class Person {
private String name;
private Integer age;
public Person() {
super();
}
public Person(String name, Integer age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz1 = Class.forName("org.hu.test.entity.Person");
Class<Person> clazz2 = Person.class;
Person p = new Person();
Class<? extends Person> clazz3 = p.getClass();
System.out.println(clazz1 == clazz2); // true
System.out.println(clazz2 == clazz3); // true
}
}
tip:三种方式获取的Class对象的泛型各不相同。
通过反射创建对象
大家都知道,创建对象需要调用类的构造方法,通过反射创建对象也不例外。
调用Class对象的newInstance()方法,就可以创建Class对象对应类的对象,而该方法的本质就是调用了无参构造方法。
public class Test {
public static void main(String[] args)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
Class<?> clazz = Class.forName("org.hu.test.entity.Person");
Object obj = clazz.newInstance();
Person p = (Person) obj;
System.out.println(p);
}
}
上面的代码运行起来没有问题。但是如果Person类没有空参构造方法,或者说Person类的空参构造方法是private的呢?所以使用Class对象的newInstance()方法是有限制的——只能调用可见的空参构造方法。这里提到的可见指的是权限修饰符的作用域(具体在这篇文章中有介绍:Java基础(4) 对象)。
所以如果想要通过反射创建对象时,没有达到上述要求,就需要另觅他径。
首先如果Person类没有空参构造,那么就可以使用Class对象提供getConstructor()方法来创建对象。该方法接受任意个Class类型参数并返回Constructor类型结果,换而言之,Person类的无参有参构造方法都可以通过方法调用。
Constructor类也提供newInstance()方法,相较于Class对象提供的newInstance()方法,Constructor类提供的newInstance()方法可以接受任意个Object类型。
getConstructor()方法和newInstance()方法的关系如下:
getConstructor(构造方法参数类型1,构造方法参数类型2,...) // 声明要使用的构造方法(该构造方法必须存在且可见)
newInstance(创建对象的参数1,创建对象的参数2,...) // 传入参数顺序对应和getConstructor()方法中的参数类型
下面通过一个例子演示:
public class Test {
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException {
Class<?> clazz = Class.forName("org.hu.test.entity.Person");
// getConstructor()方法的参数顺序要严格按照已有构造方法的参数顺序来写, 否则会抛出NoSuchMethodException
Constructor<?> c = clazz.getConstructor(String.class, Integer.class);
// Constructor<?> c = clazz.getConstructor(Integer.class, String.class); // 报错
Person p = (Person) c.newInstance("张三", 23);
System.out.println(p);
}
}
其次如果Person类中的构造方法是不可见的,例如这样:
private Person() {
super();
}
这时候Peron类是无法通过空参构造方法来创建对象的:
Person p = new Person(); // 报错:The constructor Person() is not visible
然而即使是这样,我们依然可以通过反射来调用Person类的私有空参构造方法创建对象。由于此时Peron类的空参构造方法相对于Test类是不可见的,所以这时候就需要使用Class对象的getDeclaredConstructor()方法,该方法可以获取对应类的任意一个已存在的构造方法。
但是仅调用getDeclaredConstructor()方法还是无法创建对象的,因为方法不可见还意味着对于Test类而言是无权调用该方法的。所以我们还需要设置setAccessible(true),使该方法可以被访问。具体可以参考下面的例子:
public class Test {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("org.hu.test.entity.Person");
Constructor<?> c = clazz.getDeclaredConstructor();
c.setAccessible(true);
Person p = (Person) c.newInstance();
/**
* getModifiers() 获取该方法的修饰符信息
* Modifier.toString() 翻译修饰符信息
*/
System.out.println(Modifier.toString(c.getModifiers()));
System.out.println(p);
}
}
通过反射操作成员变量和成员方法
在反射面前,每一个类都是“赤裸裸”的。所以我们不仅可以通过反射来创建对象,也可以通过反射来操作对象的所有成员变量和成员方法。
Class对象的getField()方法可以获取任意类的一个成员变量,该方法接受一个String类型参数(代表类成员变量的字段名)并返回Field类型结果,通过Field对象提供的get、set方法就可以操作对象的成员变量。下来通过一个例子演示:
public class Test {
public static void main(String[] args) throws Exception {
Person p = new Person("张三", 21);
Class<?> clazz = Class.forName("org.hu.test.entity.Person");
Field field = clazz.getField("name");
field.set(p, "李四");
System.out.println(p);
}
}
运行上面的代码,你就会发现程序报错了。如果上面讲解通过反射创建对象的时候你已经完全理解了,那么这里你应该很快就可以反应过来:因为Person类的成员变量“name”是对于Test类而言是不可见的。
所以这里我们应该使用getDeclaredField()方法来获取已声明的成员变量,然后设置setAccessible(true)使该成员变量可以被访问,就可以操作Person对象p的“name”成员变量:
public class Test {
public static void main(String[] args) throws Exception {
Person p = new Person("张三", 21);
Class<?> clazz = Class.forName("org.hu.test.entity.Person");
Field declaredField = clazz.getDeclaredField("name");
declaredField.setAccessible(true);
declaredField.set(p, "李四");
System.out.println(p);
}
}
和成员变量一样,同为类的成员,成员方法也是可以通过反射操作的。目前Person类中没有成员方法,在Person类中添加如下两个方法:
public void eat() {
System.out.println("i am eating");
}
public void eat(String food) {
System.out.println("eat " + food);
}
相信经过上面的通过反射创建对象和通过反射获取成员变量两个例子,你已经对权限修饰符有些印象了,这里就不挖坑了,两个方法都设置为public。
通过Class对象的getMethod()方法可以获取类的成员方法,该方法接受两个参数——String类型的成员方法名字和任意个Object类型的成员方法参数,返回结果是Method类型。调用Method对象的invoke()方法,就可以执行类的成员方法。具体看下面的例子:
public class Test {
public static void main(String[] args) throws Exception {
Person p = new Person("张三", 21);
Class<?> clazz = Class.forName("org.hu.test.entity.Person");
Method m = clazz.getMethod("eat", String.class);
m.invoke(p, "apple");
}
}
反射与配置文件
在文章开篇的时候提过,框架的配置文件就是通过反射来实现的。下面通过一个例子来演示配置文件是如何作用于代码的。
大家应该都知道榨汁机,想喝什么果汁的时候,把水果丢进去,就可以得到一杯果汁。下面用程序来模拟榨汁:
public interface Fruit {
void juicing();
}
public class Orange implements Fruit {
@Override
public void juicing() {
System.out.println("Orange juice");
}
}
public class Apple implements Fruit {
@Override
public void juicing() {
System.out.println("Apple juice");
}
}
public class Juicer {
public void run(Fruit f) {
f.juicing();
}
}
public class Test {
public static void main(String[] args) {
Juicer j = new Juicer();
j.run(new Apple());
// j.run(new Orange());
}
}
通过上面的代码,当我们想喝苹果汁的时候,就可以向榨汁机j中丢一个苹果,想喝橘子汁的时候就向榨汁机j中放一个橘子。
但是大家可以发现一个问题,就是我们每次改变需求的时候(喝不同的果汁)就需要去修改源码,这样是很不方便的,也不利于维护。这里我们可以使用配置文件来解决这个问题。
在项目下新建配置文件config.properties(例子中是创建在项目根目录下),在文件中写入你想要获取的类的全名:
org.hu.test.entity.Apple
然后我们就可以通过配置文件获取到类的名字,然后使用forName()方法来创建Class对象。后面如果出现需求改变,直接在配置文件修改就可以了。具体参考下面的例子:
public class Test {
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new FileReader("config.properties"));
Class<?> clazz = Class.forName(br.readLine());
Fruit f = (Fruit) clazz.newInstance();
Juicer j = new Juicer();
j.run(f);
}
}
越过泛型检查
在之前的文章提到过,泛型在编译期有效,在运行期泛型会被擦除。而我们知道,反射在运行期进行的,所以通过反射就可以越过编译期的泛型检查。具体看下面一个例子:
public class Test {
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
// list.add("abc"); // 错误:泛型检查,无法加入
Class<?> clazz = Class.forName("java.util.ArrayList");
Method method = clazz.getMethod("add", Object.class);
method.invoke(list, "abc");
System.out.println(list);
}
}