一、概述
反射就是把Java类中的各种成分映射成相应的java类。
二、反射的基石-----Class类
1.概述
1)Class类
Java类用于描述一类事物的共性,该类事物有什么属性,没有什么属性,至于这个属性的值是什么,是由这个类的对象来确定的,不同的实例对象有不同的属性值。
例如,Person类代表人,它的实例对象就是张三,李四这样一个个具体的人。
Java程序中的各个Java类属于同一类事物,描述这类事物的Java类名就是Class。
Class类描述了类的名字,类的访问属性,类所属于的包名,字段名称的列表、方法名称的列表,等等。学习反射,首先就要明白Class这个类。
2)Class类代表Java类,它的各个实例对象又分别对应什么呢?
对应各个类在内存中的字节码,例如,Person类的字节码,ArrayList类的字节码,等等。
一个类被类加载器加载到内存中,占用一片存储空间,这个空间里面的内容就是类的字节码,不同的类的字节码是不同的,
所以它们在内存中的内容是不同的,这一个个的空间可分别用一个个的对象来表示。
每个java类都是Class的一个实例对象,它们的内容不同,但是,它们的特征相同,譬如,都有方法,有字段,有父类,有包。
注意:小写class关键字用来定义类,与Class有很大的区别
2.得到各个字节码对应的实例对象( Class类型)的三种方式
1)类名.class,通过类名获得, 例如,Peron.class
2)对象.getClass(),通过对象获得,例如,new Person().getClass()
3)Class.forName(完整的包名类名),根据包含类名的字符串获得,例如,Class.forName("java.util.Date")
前两种方式都需要导入一个明确的类,如果要操作的类不明确时使用可能会受到限制,而第三种方式根据传入的字符串来操作相应的类,具有更好的扩展性。
做反射的时候主要用第三种,因为一般写程序的时候并不知道类的名字,运行的时候接收一个字符串,字符串中包含类名。
Class.forName(String className);的作用是返回字节码,返回字节码的方式有两种:
其一,内存中已经有这份字节码,直接返回字节码
其二,内存中没有这份字节码,用类加载器加载并缓存起来,然后返回字节码
所以一个类在虚拟机中通常只有一份字节码
加载了字节码,并调用了其getMethods之类的方法,但是没有看到类的静态代码块被执行,只有在第一个实例对象被创建时,这个静态代码才会被执行。
准确的说,静态代码块不是在类加载时被调用的,而是第一个实例对象被创建时才执行的。
3.九个预定义的Class实例对象
1)八种基本类型(byte、short、int、long、float、double、char、boolean)对应的字节码对象byte.class、short.class等和一种返回值为void类型的void.class
2)Integer.TYPE是Integer类的一个常量,它代表此包装类型包装的基本类型的字节码,所以和int.class是相等的。
基本数据类型的字节码都可以用与之对应的包装类中的TYPE常量表示
3)数组类型的Class实例对象int[].class、String[][].class等,可以用Class.isArray()判断是否是一个数组类型
总之,只要是在源程序中出现的类型,都有各自的Class实例对象,例如,int[],void…
4.Class类中的常用方法
1)获得Class对象
static Class(?) forName(String className); 返回与给定字符串名的类或接口相关联的Class
对象。
2)获得构造方法对象
Constructor<T> getConstructor(Class<?> ... parameterTypes);
根据传入的参数类型返回指定的Contructor对象,该构造方法必须是公有的
Constructor<T> getDeclaredConstructor(Class<?> ... parameterTypes);
根据传入的参数类型返回指定的Contructor对象,该构造方法只要在类中声明即可,不需要是公有的
Constructor<T>[ ] getConstructors(); 返回所有的公有的构造方法的对象的数组
Constructor<T>[ ] getDeclaredConstructors(); 返回该类声明的所有构造方法的对象的数组
3)获得属性对象
Field getField(String fieldName); 根据属性名获取相应的Field对象,该属性必须声明为公有的
Field getDeclaredField(String fieldName); 根据属性名获取相应的Field对象
Field[ ] getFields(); 返回所有公有属性对象的数组
Field[ ] getDeclaredFields(); 返回该类声明的所有属性对象的数组
4)获得方法对象
Method getMethod(String methodName, Class<?> ... parameterType);
根据方法名和传入的参数列表返回相应的Method对象,该方法在类中必须声明为公有的
Method getDeclaredMethod(String methodName, Class<?> ... parameterType);
根据方法名和传入的参数列表返回相应的Method对象
Method[ ] getMethods(); 返回所有公有方法对象的数组
Method[ ] getDeclaredMethods(); 返回该类声明的所有方法对象的数组
注意:获得属性和方法对象时,getXxx()表示获得该类声明的公有属性或方法和从父类继承的公有的属性或方法,
getDeclaredXxx()表示获得该类中声明的所有的属性或方法,不包含从父类继承的,但是可以得到非public修饰的
5)创建对象
T newInstance();
调用默认的无参构造方法创建对象,返回指定类型的对象,但是类中无参的构造方法必须存在
6)其他
String getName(); 返回类名
boolean isArray(); 判断是否是数组类型
boolean isPrimitive(); 判断是否是基本数据类型
三、Constructor类
Constructor类用于描述类中的构造方法,可以用来得到构造方法的名字,得到所属于的类,产生实例对象
1.得到构造方法
一个类有多个构造方法,根据参数的个数和类型可以区分清楚想得到其中的哪个方法,参数类型用Class实例对象方式表示
1)得到某个类所有公有的构造方法:
Constructor [] constructors= Class.forName("java.lang.String").getConstructors();
2)得到某一个公有的构造方法:
Constructor constructor = Class.forName(“java.lang.String”).getConstructor(StringBuffer.class);
2.创建实例对象:
1)通常方式:String str = new String(new StringBuffer("abc"));
2)反射方式: String str = (String)constructor.newInstance(new StringBuffer("abc"));
注意:1)T newInstance(Object ... initargs); 传入的参数必须与调用这个方法的Constructor对象对应的参数列表相同,
在没有加泛型的情况下,返回值为Object,需进行强制类型转换。
2)Class类中的T newInstance();是调用默认的构造方法实例化对象,默认构造方法必须存在且非私有,
T newInstance(Object ... initargs); 调用指定的构造方法实例化对象,构造方法必须是public修饰的
示例代码:
package reflect;
import java.lang.reflect.Constructor;
public class ConstructorDemo {
public static void main(String[] args) throws Exception{
//获得Class对象
Class<?> cls = Class.forName("reflect.Person");
//指定参数列表,获得Constructor对象
Constructor c = cls.getConstructor(String.class, int.class);
//传入参数,用Constructor对象创建对象
Person p = (Person)c.newInstance("hellen", 18);
System.out.println(p.toString());
}
}
/*
* Person类
*/
class Person{
String name;
int age;
public Person(){}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return this.name+":"+this.age;
}
}
四、Field类
Field类代表某个类中的一个成员变量
得到的Field对象是对应到类上面的成员变量,类只有一个,而该类的实例对象有多个,所以字段fieldX 代表的是x的定义,而不是具体的x变量。
常用方法:
1. Object get(Object obj); 获得指定对象的属性值
2. void set(Object obj, Object value); 为指定对象设置属性值
3. Class<?> getType(); 获得该属性的类型,返回Class对象
4. void setAccessible(boolean flag); 设置属性是否可访问
示例代码:
package reflect;
import java.lang.reflect.Field;
//ReflectPoint类
public class ReflectPoint {
private int x;
private int y;
public ReflectPoint() { }
public ReflectPoint(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "("+this.x+","+this.y+")";
}
public static void main(String[] args) throws Exception{
Class<?> cls = Class.forName("reflect.ReflectPoint");
//获得属性x的Field对象
Field f = cls.getDeclaredField("x");
//设置属性x为可访问
f.setAccessible(true);
//创建一个ReflectPoint对象
ReflectPoint rd = new ReflectPoint(5,6);
//获得属性值
System.out.println("通过反射获得属性值:"+f.get(rd));
//设置属性值
f.set(rd, 8);
System.out.println("通过反射设置属性值之后:"+f.get(rd));;
}
}
思考:把变量定义成private,就是不想让人家访问,可是,用暴力反射还是能够访问,前后矛盾,能不能让人家用暴力反射也访问不了我。首先,private主要是给javac编译器看的,希望在写程序的时候,在源代码中不要访问我,是帮组程序员实现高内聚、低耦合的一种策略。如果你非要去访问,那我也没办法,只得由你去访问。同样的道理,泛型集合在编译时可以帮助我们限定元素的内容,如果你不想要这种便利,绕过编译器,就可以往集合中存入另外类型了。
五、Method类
Method类代表某个类中的一个成员方法
1.常用方法:
1)Class<?> getReturnType(); 获取该方法都返回值类型,以Class对象的方式返回
2)Class<?>[ ] getParameterTypes(); 获得该方法参数列表的数组,以Class数组返回
3)Object invoke(Objec obj, Object ... args); 对带有指定参数的指定对象调用由此Method
对象表示的底层方法。
4)void setAccessible(boolean flag); 设置方法是否可访问
得到类中的某一个方法:
Method charAt = Class.forName("java.lang.String").getMethod("charAt", int.class); //该方法必须声明为public
调用方法:
通常方式:System.out.println(str.charAt(1));
反射方式: System.out.println(charAt.invoke(str, 1));
注意:如果传递给Method对象的invoke()方法的第一个参数为null,说明该Method对象对应的是一个静态方法
示例代码:
//在ReflectPoint类中增加out方法
public void out(String info){
System.out.println(info+"\nx:"+this.x+";y:"+this.y);
}
public static void main(String[] args) throws Exception{
Class<?> cls = Class.forName("reflect.ReflectPoint");
//创建一个ReflectPoint对象
ReflectPoint rd = new ReflectPoint(5,6);
//获得指定方法对象
Method m = cls.getMethod("out", String.class);
//调用方法
m.invoke(rd, "坐标为:");
}
2.jdk1.4和jdk1.5的invoke方法的区别:
Jdk1.5:public Object invoke(Object obj,Object... args)
Jdk1.4:public Object invoke(Object obj,Object[] args)
即按jdk1.4的语法,需要将一个数组作为参数传递给invoke方法时,数组中的每个元素分别对应被调用方法中的一个参数,
所以,调用charAt方法的代码也可以用Jdk1.4改写为 charAt.invoke(“str”, new Object[]{1})形式。
3.用反射方式执行某个类中的main方法
示例代码:
package reflect;
import java.lang.reflect.Method;
public class MainDemo {
public static void main(String[] args) throws Exception{
Class cls = Class.forName("reflect.MainTest");
//获得main方法的Method对象
Method mainMethod = cls.getMethod("main", String[].class);
//传入参数,调用方法
mainMethod.invoke(null, new String[]{"hello", "this", "here"});//这里传入的参数有错
}
}
class MainTest{
public static void main(String[] args) {
System.out.println("接收的参数:");
for(String str: args)
System.out.println(str);
}
}
运行截图:
错误的参数个数
启动Java程序的main方法的参数是一个字符串数组,即public static void main(String[] args),通过反射方式来调用这个main方法时,如何为invoke方法传递参数呢?按jdk1.5的语法,整个数组是一个参数,而按jdk1.4的语法,数组中的每个元素对应一个参数,当把一个字符串数组作为参数传递给invoke方法时,javac会到底按照哪种语法进行处理呢?jdk1.5肯定要兼容jdk1.4的语法,当你传一个数组给它时会按jdk1.4的语法进行处理,即把数组打散成为若干个单独的参数。所以,在给main方法传递参数时,不能使用代码mainMethod.invoke(null,new String[]{“xxx”}),javac只把它当作jdk1.4的语法进行理解,而不把它当作jdk1.5的语法解释,因此会出现参数类型不对的问题。
解决方法
1)按JDK1.4的语法传一个Object数组给它,mainMethod.invoke(null,new Object[]{new String[]{"xxx"}});
2)按JDK1.5的语法将数组类型转变成Object类型,mainMethod.invoke(null,(Object)new String[]{"xxx"}); ,
编译器会作特殊处理,编译时不把参数当作数组看待,也就不会数组打散成若干个参数了
注意:如果该方法的参数列表只有一个参数,且这个参数为对象类型的数组,如int[ ][ ],Person[ ]等等,
反射调用时必须按照上面的两种方法传入参数
六、数组的反射
1、具有相同维数和元素类型的数组属于同一个类型,即具有相同的Class实例对象。
2、代表数组的Class实例对象的getSuperClass()方法返回的父类为Object类对应的Class。
3、基本类型的一维数组可以被当作Object类型使用,不能当作Object[]类型使用;
4、非基本类型的一维数组,既可以当做Object类型使用,又可以当做Object[]类型使用。
5、Object[ ] 与String[ ]没有父子关系,Object与String有父子关系,
所以new Object[ ]{“aaa”,”bb”}不能强制转换成new String[ ]{“aaa”,”bb”}; Object x = “abc”能强制转换成String x = “abc”。
6、没有办法直接得到数组中的元素类型,需要取出每个元素对象,然后再对各个对象进行判断。
因为其中每个具体元素的类型都可以不同,例如Object[] x = new Object[]{“abc”,Integer.Max}。
7、Array工具类用于完成对数组的反射操作。
Array类常用方法
1)static Object get(Object array, int index); 获得指定数组指定下标对应的值
2)static void set(Object array, int index, Object value); 将指定数组指定下标对应的值修改为value
3)static int getLength(Object array); 获得指定数组的长度
示例代码:
public class ArrayReflect {
public static void main(String[] args) {
int[] arr1 = new int[6];
int[] arr2 = new int[9];
int[][] arr3 = new int[6][];
String[] arr4 = new String[6];
System.out.println(arr1.getClass() == arr2.getClass());
//下面的两句在编译的时候就会提示错误,因为相同类型相同维度的数组的字节码才相同
// System.out.println(arr1.getClass() == arr3.getClass());
// System.out.println(arr1.getClass() == arr4.getClass());
//所有的数组类型都可以转化成Object类型
Object o1 = arr1;
Object o2 = arr3;
Object o3 = arr4;
//下面的第一句编译时会报错,因为只有非基本类型的数组才可以转化成Object类型的数组,
//基本类型的二维数组可以看成是非基本类型的数组
// Object[] oo1 = arr1;
Object[] oo2 = arr3;
Object[] oo3 = arr4;
//得到数组的父类
Class superClass = arr3.getClass().getSuperclass();
System.out.println(superClass.getName()); //输出java.lang.Object
printObject(new char[][]{{'d', 'r'}, {'g'}});
printObject("hello");
//分析研究Arrays.asList()方法处理int[]和String[]时的差异
//传入int类型的数组输出的是哈希值,而String类型的数组数组输出的是相应的字符串
//因为JDK1.5 static <T> List<T> Arrays.asList(T ... a)
//JDK1.4 static List Arrays.asList(Object[] obj);
//JDK1.5兼容JDK1.4,当传一个基本类型的一维数组时,会被当成一个Object对象,用JDK1.5的语法解释
//如果传入非基本类型的一维数组时,会被当成一个Object类型的数组,用JDK1.4的语法解释
System.out.println(Arrays.asList(new int[]{2,4,5}));
List list = Arrays.asList(new String[]{"haha", "heihei"});
System.out.println(list);
}
/*
* 打印对象,如果是数组类型的就一次打印里面的元素
* 注意:如果是二维数组打印的结果是每一个以为数组的哈希值
*/
static void printObject(Object obj){
//获得Class对象
Class cls = obj.getClass();
//判断是否是数组类型的对象
if(cls.isArray()){
//通过反射获得数组的长度
int length = Array.getLength(obj);
for(int i=0; i<length; i++){
//通过反射获得数组下标对应的元素
System.out.println(Array.get(obj, i));
}
}else
System.out.println(obj);
}
}
七、反射的作用--->实现框架功能