Java的反射机制

在这里插入图片描述

为什么要使用反射?

我们之前已经学过接口的使用,极大的降低了代码的耦合度,并提高了可复用性和可维护性。举一个例子:
我们创建一个接口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);
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值