反射
反射的概述
一个类有多个组成部分,例如:成员变量,方法,构造方法等。反射就是加载类,并解剖出类的各个组成部分。也就是说,反射是把Java类中的各种成分映射成相应的Java类。例如,一个Java类用一个Class类的对象来表示,一个类中的组成部分:成员变量,方法,构造方法,包等等信息也用一个个的Java类来表示,就像汽车是一个类,汽车中的发动机,变速箱等等也是一个个的类。表示Java类的Class类显然要提供一系列的方法,来获得其中的变量,方法,构造方法,修饰符,包等信息,这些信息就是用相应类的实例对象来表示,它们是Field、Method、Contructor、Package等等。
一个类中的每个成员都可以用相应的反射API类的一个实例对象来表示,通过调用Class类的方法可以得到这些实例对象,得到这些实例对象后有什么用呢?怎么用呢?这正是学习和应用反射的要点。
Constructor类
Constructor类代表某个类中的一个构造方法。Constructor对象代表一个构造方法,大家觉得Constructor对象上会有什么方法呢?
通过反射获取构造方法
package cn.liayun.reflect;
import java.lang.reflect.Constructor;
public class ReflectDemo {
public static void main(String[] args) throws Exception {
//得到某个类所有的构造方法
Constructor[] constructors = Class.forName("java.lang.String").getConstructors();
//得到某一个构造方法
Constructor constructor1 = String.class.getConstructor(StringBuffer.class);
}
}
一个类会有多个构造方法,那么用什么方式可以区分清楚想得到其中的哪个方法呢?根据参数的个数和类型,例如Class.getMethod(name, Class... args)
中的args参数就代表所要获取的那个方法的各个参数的类型的列表,参数类型用Class实例对象的方式来表示。
获取指定参数的构造方法并创建对象
通常我们通过指定参数的构造方法创建对象是这样做的,
而用反射是这样做的:
获取无参的构造方法并创建对象
最便捷的方式,就是通过Class实例对象的newInstance()方法。该方法内部先得到默认的构造方法,然后用该构造方法创建实例对象,所以利用此种方式创建类对象时,类必须有一个无参的构造函数。该方法内部的具体代码是怎样写的呢?用到了缓存机制来保存默认构造方法的实例对象。
Field类
Field类代表某个类中的一个成员变量。
通过反射方式,获取指定public修饰的成员变量
下面我会定义一个ReflectPoint类来进行演示。
package cn.liayun.domain;
public class ReflectPoint {
private int x;
public int y;
public ReflectPoint(int x, int y) {
super();
this.x = x;
this.y = y;
}
}
试着运行如下测试程序代码,那你会得到5。
通过以上程序代码,相信我们能够回答这样一个问题。得到的Field对象是对应到类上面的成员变量呢,还是对应到对象上的成员变量呢?类只有一个,而该类的实例对象有多个,如果是与对象关联,哪关联的是哪个对象呢?所以字段fieldY代表的是y的定义,而不是具体的y变量。
通过反射方式,获取指定的任意成员变量(包含私有)
试着运行如下测试程序代码,那你将发现会报异常——java.lang.NoSuchFieldException: x,究其原因是该字段是用private修饰的,那要获取该成员变量又该如何呢?
Class实例对象的getField方法获取的是指定public修饰的成员变量,而getDeclaredField方法可以获取任意的成员变量,所以我们可以使用该方法。
发现还是报错,如果非要获取,怎么办,可用暴力反射。
那么问题来了,我把自己的变量定义成private,就是不想让人家访问,可是,现在人家用暴力反射还是能够访问我,这说不通啊,能不能让人家用暴力反射也访问不了我。首先,private主要是给javac编译器看的,希望在写程序的时候,在源代码中不要访问我,是帮助程序员实现高内聚、低耦合的一种策略。你这个程序员不领情,非要去访问,那我拦不住你,由你去吧。同样的道理,泛型集合在编译时可以帮助我们限定元素的内容,这是人家提供的好处,而你非不想要这个好处,怎么办?绕过编译器,就可以往集合中存入另外类型了。
练习
将任意一个对象中的所有String类型的成员变量所对应的字符串内容中的"b"改成"a"。例如,有ReflectPoint类定义如下:
package cn.liayun.domain;
public class ReflectPoint {
private int x;
public int y;
public String str1 = "ball";
public String str2 = "basketball";
public String str3 = "itcast";
public ReflectPoint(int x, int y) {
super();
this.x = x;
this.y = y;
}
@Override
public String toString() {
return str1 + ":" + str2 + ":" + str3;
}
}
将一个ReflectPoint类的实例对象中所有String类型的成员变量所对应的字符串内容中的"b"改成"a"。
package cn.liayun.reflect;
import java.lang.reflect.Field;
import cn.liayun.domain.ReflectPoint;
public class ReflectDemo {
public static void main(String[] args) throws Exception {
ReflectPoint pt1 = new ReflectPoint(3, 5);
/*
* 作业:将任意一个对象中的所有String类型的成员变量所对应的字符串内容中的"b"改成"a"。
*/
changeStringValue(pt1);
System.out.println(pt1);
}
private static void changeStringValue(Object obj) throws Exception {
Field[] fields = obj.getClass().getFields();
for (Field field : fields) {
// field.getType();//得到字段的类型
// if (field.getType().equals(String.class)) {
if (field.getType() == String.class) {//字节码只有同一份,对于字节码的比较,应该用==比较
String oldValue = (String) field.get(obj);
String newValue = oldValue.replace('b', 'a');
field.set(obj, newValue);
}
}
}
}
Method类
Method类代表某个类中的一个成员方法。
通过反射方式得到类中的某一个方法,并调用该方法
通过反射方式得到类中的某一个方法,比如String类中的charAt方法。
大家应通过思考和推理的方式来学习反射中的API。例如,Class.getMethod方法用于得到一个方法,该方法要接受什么参数呢?显然要一个方法名,而一个同名的方法有多个重载形式,用什么方式可以区分清楚想得到重载方法系列中的哪个方法呢?根据参数的个数和类型,例如,Class.getMethod(name, Class… args)中的args参数就代表所要获取的那个方法的各个参数的类型的列表。再强调一遍参数类型用Class实例对象来表示!
通常我们调用String类中的charAt方法,是这样做的,
而用反射是这样做的,
如果传递给Method对象的invoke()方法的第一个参数为null,这有着什么样的意义呢?说明该Method对象对应的是一个静态方法!静态方法调用不需要对象,所以传递null。
JDK1.4和JDK1.5中invoke方法的区别
-
JDK1.5中invoke方法的定义如下:
public Object invoke(Object obj, Object... args)
-
JDK1.4中invoke方法的定义如下:
public Object invoke(Object obj, Object[] args)
即按JDK1.4的语法,需要将一个数组作为参数传递给invoke方法,数组中的每个元素分别对应被调用方法中的一个参数。所以,调用charAt方法的代码也可以用JDK1.4改写为:
练习:用反射方式执行某个类中的main方法
写一个程序,这个程序能够根据用户提供的类名,去执行该类中的main方法。首先用普通方式调用,如下:
package cn.liayun.reflect;
public class ReflectMethodDemo {
public static void main(String[] args) throws Exception {
/*
* 写一个程序,这个程序能够根据用户提供的类名,去执行该类中的main方法。
*/
TestArguments.main(new String[] {"111","222","333"});
}
}
class TestArguments {
public static void main(String[] args) {
for (String arg : args) {
System.out.println(arg);
}
}
}
然后再使用反射的方式来调用,
发现报异常——java.lang.IllegalArgumentException: wrong number of arguments。究其原因就是此时实际调用的是main(String a1, String a2, String a3)
方法,而TestArguments类中没有接收3个String类型参数的main方法,TestArguments类中的main方法只接收一个参数,结果你调用的时候传了3个参数,所以就会报异常——错误的参数个数。
那么问题来了,启动Java程序的main方法的参数是一个字符串数组,即
通过反射方式来调用这个main方法时,如何为invoke方法传递参数呢?按JDK1.5的语法,整个数组是一个参数,而按JDK1.4的语法,数组中的每个元素对应一个参数,当把一个字符串数组作为参数传递给invoke方法时,javac会到底按照哪种语法进行处理呢?JDK1.5肯定要兼容JDK1.4的语法,会按JDK1.4的语法进行处理,即把数组打散成为若干个单独的参数。所以,在给main方法传递参数时,不能使用如下代码
javac只把它当作JDK1.4的语法进行理解,而不把它当作JDK1.5的语法解释,因此会出现参数类型不对的问题。解决办法有两种:
- 将String数组再包装进一个Object数组,这样将Object数组拆分后得到的就是String数组。说白了,就是我给你的数组,你不会当作参数,而是把其中的内容当作参数;
- 将String数组转成Object类型的对象,编译器会作特殊处理,编译时不把参数当作数组看待,也就不会将数组打散成若干个参数了。
注意,Object和String具有父子关系,但Objec[]和String[]没有父子关系,所以以下代码是错误的。
更进一步来说,用反射方式执行某个类中的main方法,就不能写以下代码。