您的位置:首页 > 编程语言 > Java开发

JavaSE实战——反射技术

2015-09-30 22:46 696 查看
转载请声明出处:http://blog.csdn.net/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设备配置信息,即可。



好了,JavaSE的反射技术基础,就讲解到这里。有问题请联系我:lichunchun4.0@gmail.com

源码下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: