JavaSE实战——反射技术

本文介绍了Java反射技术的基础概念,包括构造函数、字段属性、成员方法的获取与使用,并详细阐述了反射技术在USB扩展笔记本场景中的应用,通过配置文件动态加载和使用USB设备,提高了程序的扩展性和灵活性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载请声明出处:http://blog.youkuaiyun.com/zhongkelee/article/details/48834343

一、简介

1.反射的应用场景

     我们对Java中反射技术的理解,对日后去了解一些框架的底层原理是非常有必要的。


    比如在之前的博客中,我们提到过USB扩展笔记本的例子。

    原先的做法:实现接口,修改源代码。
    现在的做法:实现接口,配置配置文件。

    我们先复习一下,原来的做法是怎样的,代码如下:

package ustc.lichunchun.reflect.test;

public class NoteBook {
	public void run(){
		System.out.println("notebook run");
	}
	public void useUSB(USB usb){
		if(usb!=null){
			usb.open();
			usb.close();
		}
	}
}
package ustc.lichunchun.reflect.test;

public interface USB {
	void close();
	void open();
}
package ustc.lichunchun.reflect.test;

public class MouseByUSB implements USB {

	@Override
	public void close() {
		System.out.println("mouse close");
	}

	@Override
	public void open() {
		System.out.println("mouse open");
	}
}
package ustc.lichunchun.reflect.test;

public class KeyboardByUSB implements USB {

	@Override
	public void close() {
		System.out.println("keyboard close");
	}

	@Override
	public void open() {
		System.out.println("keyboard open");
	}
}
package ustc.lichunchun.reflect.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class NoteBookMain {

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		NoteBook book = new NoteBook();
		book.run();
		book.useUSB(null);
		book.useUSB(new MouseByUSB());	
	}
}
    因为有了鼠标,所以需要在源程序中,创建鼠标对象并传递给笔记本。这种做法虽然利用多态提高了程序的扩展性,但是还是要不断地修改源代码。book.useUSB(new MouseByUSB());
    需求:希望后期出现了设备以后,可不可以不用修改NoteBookMain的代码,还可以不断地加入新的USB设备。
    所以,我们这里可以通过 反射来解决问题。

2.反射的定义

    到了反射技术,我们就可以在没有类的情况下,配置文件给什么类就new什么对象(以前是有什么类,就new什么对象)。

    反射技术其实就是动态的获取类以及类中的成员,并可以调用该类的成员(以前是有什么类,就new什么对象。现在是没有类,给什么类就new什么对象)。而且将字节码文件封装成对象,并将字节码文件中的内容都封装成对象,这样便于操作这些成员。简单说:反射技术可以对一个类进行解剖。

3.反射的好处

    好处:反射技术的出现提高了程序的扩展性。

4.反射的基本步骤

    A.获得Class对象,就是获取到指定的名称的字节码文件对象。

    B.实例化对象,获得类的属性、方法或构造函数。

    C.访问属性、调用方法、调用构造函数创建对象。

反射技术中最重要的一点:要先获取到那个类。即:无论new什么对象,都需要先获取对应的字节码文件。那么,如何获取到类呢?

二、字节码文件的获取

1.Class类的由来


    发现java已对字节码文件进行了描述,用的是Class类完成的。如何获取一个字节码文件的对象呢?

2.获取Class字节码文件对象的三种方法

A.方式一:Object的getClass()方法。

    每个字节码文件对象在堆内存中都是唯一的,如下图所示:


    通过对内存中的每个实体对象,我们应该都可以获取到“它的母亲”,也就是所属的Class类。即,我们可以通过Object的getClass()方法获取运行时类。

public static void methodDemo_1(){
		
	//调用getClass()方法,要先有对象。
	Person p1 = new Person();
	Class clazz1 = p1.getClass();
	//class ustc.lichunchun.domain.Person
	
	Person p2 = new Person();
	Class clazz2 = p2.getClass();
	
	System.out.println(clazz1 == clazz2);//true,解释见上图即可。
}

以前也用到过此种方法:Object的equals()方法用到过,类似于instanceof()的效果。

方式一的弊端:发现在反射技术里,该方法不合适。因为反射技术是不明确具体类的。

B.方式二:每一个数据类型都有一个默认的静态属性:.class,用该属性即可获取。

    所有的数据类型都有自己对应的Class对象。表示方式很简单。每一个数据类型都有一个默认的静态属性:.class,用该属性就可以获取到字节码文件对象。

public static void methodDemo_2() {
	Class clazz1 = Person.class;
	Class clazz2 = Person.class;
	System.out.println(clazz1 == clazz2);//true
}

以前也用到过此种方法:多线程的静态同步代码块用到过。

方式二的弊端:该方法也不合适,虽然不用对象调用了,还是要用具体的类,才能调用静态属性。

C.方式三:使用的Class类中的方法,静态的forName(String)方法。

    在Class类中找到了forName()方法。通过名称就可以获取对应的字节码文件对象。重点。

    forName()方法去对应的classpath目录中找到指定的Person.class文件加载进内存,并封装成Class对象返回(见下图)。注意名称是类的全名,包括包名。

public static void methodDemo_3() throws ClassNotFoundException {
	String className = "ustc.lichunchun.domain.Person";
	Class clazz = Class.forName(className);
	System.out.println(clazz);//class ustc.lichunchun.domain.Person
}


    forName方法和new关键字的区别:

    new出一个对象实例来,这个过程包含了forName的过程,forName只是完成了加载类、确定连接的过程,JVM知道有这么个类了,并在内存中也给它分配了资源,类中若有static静态代码块,也会被执行,且作为初始化过程,只会执行这一次。而接下来的newInstance()就是完成了new的后半部分,类的实例化过程。

    记住:静态代码块和Class类是绑定的,Class类装载成功,就表示执行了静态代码块了,以后也就不会再走这段静态代码了。

    当然,我们不能说forName后就没有实例化类,在堆内存中是有一个Class对象的,只不过这个实例化对象和一般所用的有引用变量指向的实例(new出来的)在用法上有所不同吧。

    说到这里,我又不得不提一下JDBC的加载数据库驱动过程:Class.forName(DriverPath);

    方法DriverManager.getConnection()是在加载器列表中找寻驱动实例,这就是一种对forName加载的实例的使用方式。

    当我们已知类的路径,完全可以import它,然后new出实例来;但是一旦我们只能得到String形式的路径,就有必要采用forName方式来实例化类了。

    再扯远一点,使用import语句时,并没有加载类到内存,只是相当于告诉了JVM一个import列表,当代码中真正使用类时,会到import列表中搜寻目标类的位置,此时才加载、构造,这一点在static静态代码块语句中会得到验证。所以我们大可以用import.*的形式把一个包的类都import进来,这只是增多了import列表内容,搜索类时会稍费那么一点点资源,并不是说包中任何类的static成员不管你用没用到,都会被创建出来,不是这样的。

    1.创建对象的方式不一样,前者是使用类加载机制,后者是创建一个新类

    2.主要考虑到软件的可伸缩、可扩展和可重用等设计思想,Java中的工厂模式经常使用newInstance()方法来创建对象。

String className = readfromXMlConfig;//从xml 配置文件中获得字符串
class c = Class.forName(className);   
factory = (ExampleInterface)c.newInstance();   
    上面代码中已经不存在Example的类名称,这样的优点是无论Example怎么变化,上述代码不变。

    3.newInstance()实际上是把new这个方式分解了,首先调用Class加载方法加载某个类,然后实例化。这样的好处是可以在调用Class的静态加载方法forName时获得更好的灵活性,提供了一种降耦的手段。

    newInstance:弱类型,低效率。只能调用无参构造。

    new:强类型,相对较高。能调用任何public构造。

三、反射的用法

1.反射类的构造函数

演示如何根据给定的名称获取到指定的Class对象后,建立该类的对象。

A.方式一:利用Class类的newInstance()方法--调用空参构造函数

public static void getObject1() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
		
	//1.根据给定的类名获取Class对象。
	String className = "ustc.lichunchun.domain.Person";
	Class clazz = Class.forName(className);
	Object obj = clazz.newInstance();//Person run,创建一个Person对象。默认调用该类的空参构造函数。
}
上面三行代码就相当于:
Person p = new Person();
但是因为我们不知道类的名称是什么,所以不能这么写。

二者的内存创建过程不同:

(1)通过反射newInstance()的动态创建过程:是通过给定的类名,去classpath路径找Class字节码文件对象加载进堆内存,并newInstance()对象,不过这里默认使用的是空参构造函数。

(2)通过固定的类名直接new对象:是加载Person类进内存,堆内存开辟空间,调用构造函数进行对象初始化。

方式一的弊端如果需要反射的Person类没有空参构造函数,调用此方法就会报错。java.lang.InstantiationException:没有调用到与之对应的构造函数。(记住了,一般被反射的类通常都有空参数的构造函数

那万一给定类中没有空参数的构造函数呢?

B.方式二:先获取指定构造函数getConstructor(),再通过该构造函数进行实例化newInstance()--调用带参构造函数

public static void getObject2() throws Exception {
	/*
	 * 万一给定类中没有空参数的构造函数呢?
	 * 可以先获取指定的构造函数,再通过该构造函数进行实例化。
	 */
	String className = "ustc.lichunchun.domain.Person";
	Class clazz = Class.forName(className);
	
	//1.通过Class获取指定构造函数。比如带两个参数。注意参数类型!
	Constructor cons = clazz.getConstructor(String.class,int.class);
	
	//2.通过指定的构造器对象进行对象的初始化。
	Object obj = cons.newInstance("lichunchun",23);
}
上面这四行代码就相当于:

Person p = new Person("lichunchun",23);

2.反射类的字段属性

    通过上面构造函数的获取和对象的建立我们已经发现了,只要我们获取到了Class字节码文件对象,剩下的构造函数、字段、成员函数其实就相当于案板上待宰的猪,心肝脾肺肾都可以易如反掌的获取到。故我们也把反射技术形象的比喻为对类的解剖。

    接下来,我们开始反射字段,获取并设置类中的成员变量。主要说两点:

    A. getFiled()、getDeclaredFiled()的区别 --Class字节码文件对象中private字段的获取
    B. setAccessible(true)暴力访问 -- 特定实例对象中private字段的修改和获取

package ustc.lichunchun.reflect.field;

import java.lang.reflect.Field;


public class ReflectFieldDemo {

	public static void main(String[] args) throws Exception {
		/*
		 * 获取并设置类中的成员变量。
		 * 反射字段。
		 */
		getFieldDemo();
	}

	public static void getFieldDemo() throws Exception {
		String className = "ustc.lichunchun.domain.Person";
		Class clazz = Class.forName(className);
		
		//获取指定age字段。需求是是获取Class对象中的字段,和new不new对象无关。
		//Field field = clazz.getField("age"); //java.lang.NoSuchFieldException
		//getField()方法只获取公有的字段(包括父类的),而age是私有的字段。
		
		Field field = clazz.getDeclaredField("age");
		//getDeclaredField()方法获取子类自身公有、私有、默认、保护的字段(不包括继承的)。
		
		//System.out.println(field);//private int ustc.lichunchun.domain.Person.age
		============================================================================
		//要对非静态的字段操作必须有对象。
		Object obj = clazz.newInstance();
		
		//使用Field的父类AccessibleObject的setAccessible(true)方法将访问权限检查功能取消。
		field.setAccessible(true);//暴力访问
		
		field.set(obj, 40);//如果没有暴力访问,则java.lang.IllegalAccessException
		
		System.out.println(field.get(obj));//40。如果没有暴力访问,则java.lang.IllegalAccessException
		//无效访问,因为age这个field字段是私有的。
		
		//上述代码等效于:
		//Person p = new Person();
		//p.age = 23;
	}
}

3.反射类的成员方法

    首先我把我们用到的Person类代码贴出来:

package ustc.lichunchun.domain;

public class Person {
	private String name;
	private int age;
	
	public Person() {
		super();
		System.out.println("Person run");
	}

	public Person(String name, int age) {
		this.name = name;
		this.age = age;
		System.out.println("Person run run");
	}
	
	public void show(){
		System.out.println("Person show run");
	}
	
	public static void staticShow(){
		System.out.println("Person static show run");
	}
	
	public void paramShow(String name, int age){
		System.out.println("show:"+name+"---"+age);
	}
}

    下面,我们来演示三种不同情况下的反射方法。

A.反射非静态、无参数的成员方法

//反射方法。非静态,无参数的show方法。
public static void getMethodDemo1() throws Exception {
	String className = "ustc.lichunchun.domain.Person";
	Class clazz = Class.forName(className);
	
	Method method = clazz.getMethod("show", null);
	Object obj = clazz.newInstance();
	
	method.invoke(obj, null);//Person show run
	
	//Person p = new Person();
	//p.show();
}

B.反射静态、无参数的成员方法

//反射方法。静态,无参数的show方法。
public static void getMethodDemo2() throws Exception {
	String className = "ustc.lichunchun.domain.Person";
	Class clazz = Class.forName(className);
	
	Method method = clazz.getMethod("staticShow", null);
	//静态方法,不需要创建对象,既可以调用。
	
<span style="white-space:pre">	</span>method.invoke(null, null);//Person static show run
}

C.反射非静态、有参数的成员方法

//反射方法。非静态,有参数的show方法。
public static void getMethodDemo3() throws Exception {
	String className = "ustc.lichunchun.domain.Person";
	Class clazz = Class.forName(className);
	
	Method method = clazz.getMethod("paramShow", String.class, int.class);
	Object obj = clazz.newInstance();
	
	method.invoke(obj, "lichunchun", 23);//show:lichunchun---23
}

四、反射应用场景的解决

    现在,我们在回过头来看Java反射技术的应用场景的解决。

package ustc.lichunchun.reflect.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class NoteBookMain {

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		NoteBook book = new NoteBook();
		book.run();
		book.useUSB(null);
		
		//因为有了鼠标,所以需要在源程序中,创建鼠标对象并传递给笔记本。
		//虽然利用多态提高了程序的扩展性,但是还是要不断地修改源代码。
		//book.useUSB(new MouseByUSB());
		
		//需求:希望后期出现了设备以后,可不可以不用修改NoteBookMain的代码,还可以不断地加入新的USB设备。
		//通过反射来解决问题。
		
		//1.对外提供配置文件。
		File configFile = new File("usb.properties");
		if(!configFile.exists()){
			configFile.createNewFile();
		}
		
		//2.读取流和配置文件关联。
		FileInputStream fis = new FileInputStream(configFile);
		Properties prop = new Properties();
		
		//3.将流中的数据加载到prop。
		prop.load(fis);
		
		for (int x = 1; x <= prop.size(); x++) {
			String className = prop.getProperty("usb" + x);
			
			//4.对指定的类进行加载。
			Class clazz = Class.forName(className);
			try {
				USB usb = (USB)clazz.newInstance();
				book.useUSB(usb);
			} catch (Exception e) {
				System.out.println("配置文件错误,指定类没有正确实现USB接口");
			}
		}
		fis.close();		
	}
}

    我们只需在usb.properties文件中添加相应的USB设备配置信息,即可。


     关于反射,还有一个知识点,就是用类加载器的方式管理资源和配置文件,具体用到的是Class类的getClassLoader().getReasourceAsStream()方法、Class类本身的getResourceAsStream()方法。具体可以详见张孝祥老师的高新技术部分。

InputStream ips = new FileInputStream("config.properties");
/*一个类加载器能加载.class文件,那它当然也能加载classpath环境下的其他文件,既然它有如此能力,它没有理由不顺带提供这样一个方法。它也只能加载classpath环境下的那些文件。注意:直接使用类加载器时,不能以/打头。*/

InputStream ips = ReflectTest2.class.getClassLoader().getResourceAsStream("cn/itcast/javaenhance/config.properties");
//Class提供了一个便利方法,用加载当前类的那个类加载器去加载相同包目录下的文件

InputStream ips = ReflectTest2.class.getResourceAsStream("config.properties");

InputStream ips = ReflectTest2.class.getResourceAsStream("/cn/itcast/javaenhance/config.properties");

    好了,JavaSE的反射技术基础,就讲解到这里。内省的知识点,我会在后续的高新技术部分讲解。有问题请联系我:lichunchun4.0@gmail.com

    源码下载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值