为什么要使用反射?
我们之前已经学过接口的使用,极大的降低了代码的耦合度,并提高了可复用性和可维护性。举一个例子:
我们创建一个接口X拥有test方法,还有两个接口实现类A,B
public class Test {
interface X {
public void test();
}
class A implements X{
@Override
public void test() {
System.out.println("I am A");
}
}
class B implements X{
@Override
public void test() {
System.out.println("I am B");
}
}
一般来说,我们需要用到哪个类的test方法,就new一个类的对象好了
public class Test {
......
public static void main(String[] args) {
X a = create1("A");//向上转型,忘记可以查看多态的实现
a.test();
X b = create1("B");
b.test();
}
public static X create1(String name){
if (name.equals("A")) {
return new A();
} else if(name.equals("B")){
return new B();
}
return null;
}
}
但是有一个问题,如果有上千个类都实现了X接口的test方法,那么我们岂不是要在create1方法里加上千个if语句。。
这时候反射机制就出现了,我们看看反射怎么处理这种情况
public class Test {
public static void main(String[] args) {
X a = create2("A");
a.test();
X b = create2("B");
b.testReflect();
}
// 使用反射机制
public static X create2(String name){
Class<?> class = Class.forName(name);//1.返回一个Class类
X x = (X) class.newInstance();//2.创建一个对应的实例
//上面两行代码其实等价与X x=new A();
return x;
}
}
向 create2() 方法传入包名和类名,通过反射机制动态的加载指定的类,然后再实例化对象。create2不管有多少个类实现了X接口,都不像create1一样需要改动方法体。代码的整洁性非常高
反射的四大功能
1.在运行时(动态编译)获知任意一个对象所属的类。
2.在运行时构造任意一个类的对象。
3.在运行时获知任意一个类所具有的成员变量和方法。
4.在运行时调用任意一个对象的方法和属性。
上述这种动态获取信息、动态调用对象的方法的功能称为 Java 语言的反射机制。(发生在JVM编译过程)
什么是Class类
要想理解反射,首先要理解 Class 类,因为 Class 类是反射实现的基础。
在程序运行期间,JVM始终为所有的对象维护一个被称为运行时的类型标识的标识符,这个信息跟踪着每个对象所属的类的完整结构信息,包括包名、类名、实现的接口、拥有的方法和字段等。可以通过专门的 Java 类访问这些信息,这个类就是 Class 类。Class类可以理解为类的类型,一个 Class 对象,称为类的类型对象,一个 Class 对象对应一个加载到 JVM 中的一个 .class 文件。
正常情况下,我们的认知都认为是先有类,然后才可以创建对象,下面是正常的类加载过程
import java.util.Date; // 先有类
public class Test {
public static void main(String[] args) {
Date date = new Date(); // 后有对象
System.out.println(date);
}
}
首先JVM会将.java文件编译成.class字节码文件,然后被类加载器(Class Loader)加载进JVM的内存里,同时还会创建一个Date类的Class对象存放到Java堆中(这个Class就是上述说的类的类型对象,不是new出来的)。JVM在创建Date对象之前,会检查Date类是否已经加载过,如果加载过了就在堆中分配内存,然后再执行new Date()语句。
需要注意的是,每个类只可以有一个Class对象,也就是说我们即使执行了第二条new Date()语句,也不会再新建一个Class对象,我们可以用 == 来验证
(类的对象可以有很多个,但是类的类型对象(Class对象)只有一个)
System.out.println(date.getClass() == Date.getClass()); // true
那么在加载完Date类之后,我们的Java堆内存的方法区就有了一个Class对象,这个对象就包含了完整的类的结构信息,我们可以通过这个 Class 对象看到类的结构,就好比一面镜子。所以我们形象的称之为:反射。
详细点说,上文说的先有类再有对象我们认为是正,那么反射的反其实就代表通过对象找到对象所属的类
Date date = new Date();
System.out.println(date.getClass()); // "class java.util.Date"
获取Class对象的四种方式
从Class的构造函数可以看出,它的构造方法是私有的,所以只有JVM可以创建Class对象,我们是不能通过new关键字构造Class对象的
我们只能通过已经加载好的类来获得一个Class对象,Java提供4种方式
1.知道具体的类是什么
Class alunbarClass = TargetObject.class;
但是这种情况一般很少见,基本都是通过遍历包下面的类来获取Class对象
这个方式获得的Class对象不会初始化
2.使用Class.forname(xxx)传入全类名获取
Class alunbarClass1 = Class.forName("com.xxx.TargetObject");
forname方法内部实际是调用的forname0
第二个参数true代表需要初始化,默认也是true,一旦初始化,就会触发目标对象的 static 块代码执行,static 参数也会被再次初始化。
3.通过对象实例instance.getclass获取
Date date = new Date();
Class alunbarClass2 = date.getClass();
// 获取该对象实例的 Class 类对象
4.通过类加载器xxxClassLoader.loadClass传入类路径获取
Class clazz = ClassLoader.LoadClass("com.xxx.TargetObject");
类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一些列步骤,静态块和静态对象不会得到执行。这里可以和 forName 做个对比。
如何通过反射构造一个类的实例(构造对象)
上部分我们已经知道如何获取一个类的类型对象了(Class对象),那么成功获取之后,我们怎么通过这个Class对象构造我们想要的类的实例呢?
1.使用Class.newInstance
Date date1 = new Date();
Class alunbarClass2 = date1.getClass();
Date date2 = alunbarClass2.newInstance(); // 创建一个与 alunbarClass2 具有相同类类型的实例
需要注意的是,**newInstance 方法调用默认的构造函数(无参构造函数)初始化新创建的对象。**如果这个类没有默认的构造函数, 就会抛出一个异常。
2.通过反射先获取构造方法,然后调用构造方法创建
由于不是所有的类都有无参构造函数又或者类构造器是 private 的,在这样的情况下,如果我们还想通过反射来实例化对象,Class.newInstance 是无法满足的。
此时,我们可以使用 Constructor 的 newInstance 方法来实现,先获取构造函数,再执行构造函数。
Constructor.newInstance 是可以携带参数的,而 Class.newInstance 是无参的,这也就是为什么它只能调用无参构造函数的原因了。
需要注意的是,如果被调用的类的构造函数是默认构造函数,使用Class.newInstance是更方便的,只有当需要调用一些private或者带参数的构造函数时才使用Constuctor.newInstance
Constructor.newInstance 是执行构造函数的方法。我们来看看获取构造函数可以通过哪些渠道,作用如其名,以下几个方法都比较好记也容易理解,返回值都通过 Cnostructor 类型来接收。
批量获取构造函数
1.获取所有的公有构造函数
public Constructor[] getConstructors(){}
2.获取所有的构造函数(默认、受保护、私有、公有)
public Constructor[] getDeclaredConstructors() { }
获取单个构造函数
1.获取一个指定参数类型的公有构造函数
public Constructor getConstructor(Class... parameterTypes){}
2.获取一个指定参数类型的构造函数(访问权限不限)
public Constructor getDeclaredConstructor(Class... parameterTypes) { }
举个例子
package fanshe;
public class Student {
//(默认的构造方法)
Student(String str){
System.out.println("(默认)的构造方法 s = " + str);
}
// 无参构造方法
public Student(){
System.out.println("调用了公有、无参构造方法执行了。。。");
}
// 有一个参数的构造方法
public Student(char name){
System.out.println("姓名:" + name);
}
// 有多个参数的构造方法
public Student(String name ,int age){
System.out.println("姓名:"+name+"年龄:"+ age);//这的执行效率有问题,以后解决。
}
// 受保护的构造方法
protected Student(boolean n){
System.out.println("受保护的构造方法 n = " + n);
}
// 私有构造方法
private Student(int age){
System.out.println("私有的构造方法年龄:"+ age);
}
}
----------------------------------
public class Constructors {
public static void main(String[] args) throws Exception {
// 加载Class对象
Class clazz = Class.forName("fanshe.Student");
// 获取所有公有构造方法
Constructor[] conArray = clazz.getConstructors();
for(Constructor c : conArray){
System.out.println(c);
}
// 获取所有的构造方法(包括:私有、受保护、默认、公有)
conArray = clazz.getDeclaredConstructors();
for(Constructor c : conArray){
System.out.println(c);
}
// 获取公有、无参的构造方法
// 因为是无参的构造方法所以类型是一个null,不写也可以:这里需要的是一个参数的类型,切记是类型
// 返回的是描述这个无参构造函数的类对象。
Constructor con = clazz.getConstructor(null);
Object obj = con.newInstance(); // 调用构造方法
// 获取私有构造方法
con = clazz.getDeclaredConstructor(int.class);
System.out.println(con);
con.setAccessible(true); // 为了调用 private 方法/域 我们需要取消安全检查
obj = con.newInstance(12); // 调用构造方法
}
}
** 使用开源库 Objenesis**
这个开源库和第二个方法一样可以调用所有的构造函数,封装的很简洁,了解一下即可
public class Test {
// 不存在无参构造函数
private int i;
public Test(int i){
this.i = i;
}
public void show(){
System.out.println("test..." + i);
}
}
------------------------
public static void main(String[] args) {
Objenesis objenesis = new ObjenesisStd(true);
Test test = objenesis.newInstance(Test.class);
test.show();
}
通过反射获取成员变量并使用
和获取构造函数(Constructor)差不多,获取成员变量也分批量获取和单个获取。返回值通过 Field 类型来接收。
批量获取
1.获取所有的公有字段
public Field[] getFields(){}
2.获取所有的字段(私有、默认。。)
public Field[] getDeclaredFields(){}
单个获取
1.获取一个指定名称的公有字段
public Field getField(String name){}//这里的name是字段的名称
2.获取一个指定名称的字段
public Field getDeclaredField(String name){}
获取到成员变量之后我们该如何改变它们的值呢?——set方法
set方法包含两个参数
obj:哪个对象要修改这个成员变量
value:要修改成哪个值
举个例子
package fanshe.field;
public class Student {
public Student(){
}
public String name;
protected int age;
char sex;
private String phoneNum;
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + ", sex=" + sex
+ ", phoneNum=" + phoneNum + "]";
}
}
----------------------------------
public class Fields {
public static void main(String[] args) throws Exception {
// 获取 Class 对象
Class stuClass = Class.forName("fanshe.field.Student");
// 获取公有的无参构造函数
Constructor con = stuClass.getConstructor();
// 获取私有构造方法
con = clazz.getDeclaredConstructor(int.class);
System.out.println(con);
con.setAccessible(true); // 为了调用 private 方法/域 我们需要取消安全检查
obj = con.newInstance(12); // 调用构造方法
// 获取所有公有的字段
Field[] fieldArray = stuClass.getFields();
for(Field f : fieldArray){
System.out.println(f);
}
// 获取所有的字段 (包括私有、受保护、默认的)
fieldArray = stuClass.getDeclaredFields();
for(Field f : fieldArray){
System.out.println(f);
}
// 获取指定名称的公有字段
Field f = stuClass.getField("name");
Object obj = con.newInstance(); // 调用构造函数,创建该类的实例
f.set(obj, "刘德华"); // 为 Student 对象中的 name 属性赋值
// 获取私有字段
f = stuClass.getDeclaredField("phoneNum");
f.setAccessible(true); // 暴力反射,解除私有限定
f.set(obj, "18888889999"); // 为 Student 对象中的 phoneNum 属性赋值
}
}
通过反射获取成员方法并使用
我们已经知道用Constructor接收构造函数,Field接受成员字段,那么接受成员方法的是?——Method
批量获取
1.获取所有的公有成员方法(包含父类的方法,所有Object的方法肯定也包含)
public Method[] getMethods(){}
2.获取所有的成员方法,包括私有的(不包括继承的)
public Method[] getDeclaredMethods() { }
单个获取
1.获取一个指定方法名和参数类型的公有成员方法:
public Method getMethod(String name, Class<?>... parameterTypes)
2.获取一个指定方法名和参数的私有成员方法:
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
获取到成员方法之后该如何调用他们?——invoke
invoke方法包含两个参数
obj:哪个对象要来调用这个方法
args:调用方法时所传递的实参
举个例子:
package fanshe.method;
public class Student {
public void show1(String s){
System.out.println("调用了:公有的,String参数的show1(): s = " + s);
}
protected void show2(){
System.out.println("调用了:受保护的,无参的show2()");
}
void show3(){
System.out.println("调用了:默认的,无参的show3()");
}
private String show4(int age){
System.out.println("调用了,私有的,并且有返回值的,int参数的show4(): age = " + age);
return "abcd";
}
}
-------------------------------------------
public class MethodClass {
public static void main(String[] args) throws Exception {
// 获取 Class对象
Class stuClass = Class.forName("fanshe.method.Student");
// 获取公有的无参构造函数
Constructor con = stuClass.getConstructor();
// 获取所有公有方法
stuClass.getMethods();
Method[] methodArray = stuClass.getMethods();
for(Method m : methodArray){
System.out.println(m);
}
// 获取所有的方法,包括私有的
methodArray = stuClass.getDeclaredMethods();
for(Method m : methodArray){
System.out.println(m);
}
// 获取公有的show1()方法
Method m = stuClass.getMethod("show1", String.class);
System.out.println(m);
Object obj = con.newInstance(); // 调用构造函数,实例化一个 Student 对象
m.invoke(obj, "小牛肉");
// 获取私有的show4()方法
m = stuClass.getDeclaredMethod("show4", int.class);
m.setAccessible(true); // 解除私有限定
Object result = m.invoke(obj, 20);
System.out.println("返回值:" + result);
}
}
反射的优缺点
优点:比较灵活,能够在运行时动态获取类的实例。
缺点:
1)性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 Java 代码要慢很多。
2)安全问题:反射机制破坏了封装性,因为通过反射可以获取并调用类的私有方法和字段。
反射的应用场景
反射在我们实际编程中其实并不会直接大量的使用,但是实际上有很多设计都与反射机制有关,比如:
1.动态代理机制
2.使用 JDBC 连接数据库
3.Spring / Hibernate 框架(实际上是因为使用了动态代理,所以才和反射机制有关)
JDBC连接数据库
1.通过 Class.forName() 加载数据库的驱动程序 (通过反射加载)
2.通过 DriverManager 类连接数据库,参数包含数据库的连接地址、用户名、密码
3.通过 Connection 接口接收连接
4.关闭连接
public static void main(String[] args) throws Exception {
Connection con = null; // 数据库的连接对象
// 1. 通过反射加载驱动程序
con=Class.forName("com.mysql.jdbc.Driver");
// 2. 连接数据库
con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test","root","root");
// 3. 关闭数据库连接
con.close();
}
Spring框架
反射机制是 Java 框架设计的灵魂,框架的内部都已经封装好了,我们自己基本用不着写。典型的除了Hibernate 之外,还有 Spring 也用到了很多反射机制,最典型的就是 Spring 通过 xml 配置文件装载 Bean(创建对象),也就是 Spring 的 IoC
1.加载配置文件,获取 Spring 容器
2.使用反射机制,根据传入的字符串获得某个类的 Class 实例
// 获取 Spring 的 IoC 容器,并根据 id 获取对象
public static void main(String[] args) {
// 1.使用 ApplicationContext 接口加载配置文件,获取 spring 容器
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
// 2. 使用反射机制,根据这个字符串获得某个类的 Class 实例
IAccountService aService = (IAccountService) ac.getBean("accountServiceImpl");
System.out.println(aService);
}