一般而言,开发者社群说到动态语言,大致认同的一个定义是:“程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言”。从这个观点看,Perl,Python,Ruby是动态语言,C++,Java,C#不是动态语言。
尽管在这样的定义与分类下Java不是动态语言,它却有着一个非常突出的动态相关机制:Reflection。这个字的意思是“反射、映象、倒影”,用在Java身上指的是我们可以于运行时加载、探知、使用编译期间完全未知的classes。换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体、或对其fields设值、或唤起其methods。
反射的基石-----Class类
Class是Reflection起源。针对任何想探勘的class,唯有先为它产生一个Class object,接下来才能经由后者唤起为数十多个的Reflection APIs。
Java中类用于描述一类事物的共性,该类事物有什么属性,没有什么属性,至于这个属性的值是什么,则是由这个类的实例对象来确定的,不同的实例对象有不同的属性值。Java程序中的各个Java类,它们是否属于同一类事物,是不是可以用一个类来描述这类事物呢?这个类的名字就是Class,注意与小写class关键字的区别。Class类描述了哪些方面的信息呢?类的名字,类的访问属性,类所属于的包名,字段名称的列表、方法名称的列表,等等。
学习反射,首先就要明白Class这个类。写如下代码进行对比理解:
1 Person p1 = new Person("zhangsan"); 2 Person p2 = new Person("lisi"); 3 /* 4 Class x1 = Vector类在内存里的字节码 5 Class x2 = Date类在内存里的字节码*/ 6 7 Class x1 = Vector.class; 8 Class x2 = Date.class;
Person类代表人,它的实例对象就是张三,李四这样一个个具体的人。
Java程序中的各个Java类属于同一类事物,描述这类事物的Java类名就是Class。
Class类的各个实例对象又分别对应什么呢?对应各个类在内存中的字节码(类模版。虚拟机只会产生一份字节码, 用这份字节码可以产生多个实例对象)。例如,Person类的字节码,ArrayList类的字节码,等等。
一个类被类加载器加载到内存中,占用一片存储空间,这个空间里面的内容就是类的字节码,不同的类的字节码是不同的,所以它们在内存中的内容是不同的,这一个个的空间可分别用一个个的对象来表示,这些对象显然具有相同的类型,这个类型是什么呢?Class类型。
如何获取一个类的字节码?
- 类名.class,例如,System.class
- 对象.getClass(),例如,new Date().getClass()
- Class.forName("类名"),例如,Class.forName("java.util.Date")
8个基本数据类型也有对应的Class对象。void也有对应的Class对象,例如,Class v = void.class;所以共有9个预定义的Class对象。
isPrimitive(),判断是否是基本数据类型。基本类型获得字节码,例如,Class i = int.class;注意,基本类型的字节码和包装类的字节码不一样,例如,int.class和Integer.class对应的字节码不一样。但是,Integer.TYPE表示包装类内部基本数据类型的字节码,所以int.class 和Integer.TYPE是一致的。
基本数据类型组成的数组,也有对应的字节码,是Class的实例对象,但不是基本类型,是数组类型,可以用isArray()判断是否是数组类型。
1 public class ReflectTest { 2 3 public static void main(String[] args) throws Exception { 4 5 String str1 = "abc"; 6 Class cls1 = str1.getClass(); 7 Class cls2 = String.class; 8 Class cls3 = Class.forName("java.lang.String"); 9 System.out.println(cls1 == cls2);//true 10 System.out.println(cls1 == cls3);//true 11 12 System.out.println(cls1.isPrimitive());//false 13 System.out.println(int.class.isPrimitive());//true 14 System.out.println(int.class == Integer.class);//false 15 System.out.println(int.class == Integer.TYPE);//true 16 System.out.println(int[].class.isPrimitive());//false 17 System.out.println(int[].class.isArray());//true 18 } 19 }
反射机制的应用
在JDK中,主要由以下类来实现Java反射机制,这些类都位于java.lang.reflect包中:
Field 类:代表类的成员变量(或叫字段或属性)。
Method 类:代表类的方法。
Constructor 类:代表类的构造函数。
Array 类:提供了动态创建数组,以及访问数组的元素的静态方法。
Annotation 类:通过Annotation[] arr = Fileld.getAnnotations();获得字段的注释;
Constructor类代表某个类中的一个构造方法。通过调用Class中的方法获得Constructor对象。
Constructor<T> |
getConstructor(Class<?>... parameterTypes) 返回一个 Constructor 对象,它反映此 Class 对象所表示的类的指定公共构造方法。 |
Constructor<?>[] |
getConstructors() 返回一个包含某些 Constructor 对象的数组,这些对象反映此 Class 对象所表示的类的所有公共构造方法。 |
得到某个类所有的构造方法:
例子:Constructor [] conall= Class.forName("java.lang.String").getConstructors();
得到某一个构造方法:
例子:Constructor con1= Class.forName(“java.lang.String”).getConstructor(StringBuffer.class); //此处StringBuffer.class是传入获得构造方法时要用到的类型
通过得到的构造方法创建实例对象:
String str = (String)con1.newInstance(new StringBuffer("abc"));//此处StringBuffer是调用获得的方法时要用到上面指定的相同类型的实例对象。
参考下面API使用此 Constructor
对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例。个别参数会自动解包,以匹配基本形参,必要时,基本参数和引用参数都要进行方法调用转换。
T |
newInstance(Object... initargs) 使用此 Constructor 对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例。 |
Class.newInstance()方法:
例子:String str= (String)Class.forName("java.lang.String").newInstance();
Class类的newInstance()方法,用于创建空参数的实例对象。底层仍然是调用Constructor类的newInstance()方法,使用缓冲机制保存实例对象,等到使用时提供出去,非常占用资源。
Field类代表某个类中的一个成员变量
Field |
getField(String name) 返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定公共成员字段。 |
Field[] |
getFields() 返回一个包含某些 Field 对象的数组,这些对象反映此 Class 对象所表示的类或接口的所有可访问公共字段。 |
Field |
getDeclaredField(String name) 返回一个 Field 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明字段。 |
Field[] |
getDeclaredFields() 返回 Field 对象的一个数组,这些对象反映此 Class 对象所表示的类或接口所声明的所有字段。 |
Object |
get(Object obj) 返回指定对象上此 Field 表示的字段的值。 |
void |
set(Object obj, Object value) 将指定对象变量上此 Field 对象表示的字段设置为指定的新值。 |
void |
setAccessible(boolean flag) 将此对象的 accessible 标志设置为指示的布尔值。 |
AccessibleObject类是 Field、Method 和 Constructor 对象的基类。它提供了将反射的对象标记为在使用时取消默认 Java 语言访问控制检查的能力。对于公共成员、默认(打包)访问成员、受保护成员和私有成员,在分别使用 Field、Method 或 Constructor 对象来设置或获取字段、调用方法,或者创建和初始化类的新实例的时候,会执行访问检查。
假如有一个类ReflectPoin。操作需求如下
1.通过反射获取Reflect类中的成员变量x和y的值
2.将Reflect类中所有字符串变量中内容中的b改成a
1 public class Reflect { 2 private int x; 3 public int y; 4 public String str1 = "ball"; 5 public String str2 = "basketball"; 6 public String str3 = "allstar"; 7 8 public Reflect(int x, int y) { 9 super(); 10 this.x = x; 11 this.y = y; 12 } 13 14 public String toString(){ 15 return str1 + ":" + str2 + ":" + str3; 16 } 17 }
1 /*需求 2 1.通过反射获取Reflect类中的成员变量x和y的值 3 2.将Reflect类中所有字符串变量中内容中的b改成a 4 */ 5 import java.lang.reflect.*; 6 class ReflectDemo 7 { 8 public static void main(String[] args) throws Exception 9 { 10 //通过构造函数对变量x和y传值 11 Reflect r1 = new Reflect(3,5); 12 //通过反射获取y的值 13 Field fieldY = r1.getClass().getField("y"); 14 //fieldY的值是多少?是5,错!fieldY不是对象身上的变量,而是类上的变量,要用它去取某个对象上对应的值。 15 System.out.println(fieldY.get(r1)); 16 //通过反射获取y的值 17 Field fieldX = r1.getClass().getDeclaredField("x"); 18 fieldX.setAccessible(true);//对于私有变量,需要“暴力反射”才能获得对应值 19 System.out.println(fieldX.get(r1)); 20 21 22 changeStringValue(r1); 23 System.out.println(r1); 24 } 25 public static void changeStringValue(Object obj) throws Exception { 26 Field[] fields = obj.getClass().getFields(); 27 for(Field field : fields){//迭代所有返回的Field字段 28 //if(field.getType().equals(String.class)){比较字节码用== 29 if(field.getType() == String.class){ 30 //如果是String.class类型,通过字段获取变量值 31 String oldValue = (String)field.get(obj); 32 String newValue = oldValue.replace('b', 'a'); 33 field.set(obj, newValue); 34 } 35 } 36 37 } 38 } 39 ---------- 运行 ---------- 40 5 41 3 42 aall:aasketaall:allstar 43 44 输出完成 (耗时 0 秒) - 正常终止
Method类代表某个类中的一个成员方法。
Class.getMethod方法用于得到一个方法,该方法要接受什么参数呢?显然要一个方法名,而一个同名的方法有多个重载形式,用什么方式可以区分清楚想得到重载方法系列中的哪个方法呢?根据参数的个数和类型。
例如,Class.getMethod(name,Class... args)中的args参数就代表所要获取的那个方法的各个参数的类型的列表。再强调一遍参数类型用什么来表示啊?用Class对象!
Object |
invoke(Object obj, Object... args) 对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。 |
1 import java.lang.reflect.*; 2 class ReflectMethod 3 { 4 public static void main(String[] args) throws Exception 5 { 6 String str1 = "abc"; 7 String str2 = "def"; 8 int x =123; 9 10 Method m1 = Class.forName("java.lang.String").getMethod("concat", String.class); 11 Method m2 = Class.forName("java.lang.String").getMethod("charAt", int.class); 12 Method m3 = Class.forName("java.lang.String").getMethod("valueOf",int.class); 13 //格式(方法名,参数对象) 14 //参数类型用Class对象表示! 15 16 System.out.println(m1.invoke(str1, str2)); 17 System.out.println(m2.invoke(str2, 1)); 18 //如果传递给Method对象的invoke()方法的第一个参数为null。说明该Method对象对应的是一个静态方法! 19 System.out.println(m3.invoke(null, x)); 20 } 21 } 22 ---------- 运行 ---------- 23 abcdef 24 e 25 123456 26 27 输出完成 (耗时 0 秒) - 正常终止
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(“str2”, new Object[]{1})形式。
JDK1.5为了兼容JDK1.4,如果传入一个数组,会默认为Object数组,自动拆包,将数组中的元素作为参数。将上例的数组拆包成2个字符串对象。为了防止这种情况,处理方法有二种:
1、将数组变成一个Object数组对象,例如,new Object[]{(new String[]{"kk","qq"})}。因为数组也属于Object类的子类。
2、转换数组的类型,例如,(Object)new String[]{"kk","qq"}。
数组的反射
有相同维数和元素类型的数组属于同一个类型,即具有相同的Class实例对象。
代表数组的Class实例对象的getSuperClass()方法返回的父类为Object类对应的Class。
基本类型的一维数组可以被当作Object类型使用,不能当作Object[]类型使用;非基本类型的一维数组,既可以当做Object类型使用,又可以当做Object[]类型使用。
Arrays的asList()方法在处理基本类型数组(int[])和引用类型数组(String[])时的差异:
1 import java.lang.reflect.*; 2 import java.util.*; 3 class ReflectMethod 4 { 5 public static void main(String[] args) throws Exception 6 { 7 int [] a1 = new int[]{1,2,3}; 8 String [] a4 = new String[]{"a","b","c"}; 9 10 System.out.println(Arrays.asList(a1)); 11 System.out.println(Arrays.asList(a4)); 12 } 13 }
可以将引用数据的数组变成集合,数组中的元素变成集合中的元素;会将整个基本数据的数组变成一个Object对象。因为JDK1.4中,asList()接受的是数组Object[];JDK1.5中,asList()接受的是可变参数T…a,T可以是基本型或引用型。例如,a4按照1.4处理,直接打印;a1按照1.5处理,打印出哈希值。
Array类提供了动态创建和静态访问数组元素的方法。
static Object |
newInstance(Class<?> componentType, int... dimensions) 创建一个具有指定的组件类型和维度的新数组。 |
static Object |
newInstance(Class<?> componentType, int length) 创建一个具有指定的组件类型和长度的新数组。 |
1 import java.lang.reflect.*; 2 public class ArrayTester1 { 3 4 public static void main(String args[]) throws Exception { 5 6 Class<?> classType = Class.forName("java.lang.String"); 7 // 创建一个长度为10的字符串数组 8 Object array = Array.newInstance(classType, 10); 9 // 把索引位置为5的元素设为"hehe" 10 Array.set(array, 5, "hehe"); 11 // 获得索引位置为5的元素的值 12 System.out.println((String)Array.get(array, 5)); 13 } 14 }
1 import java.lang.reflect.*; 2 public class ArrayTester2 { 3 public static void main(String args[]) throws Exception 4 { 5 6 int[] dims = new int[]{8, 9, 10}; 7 8 //创建一个具有指定的组件类型和维度的新数组。三维数组。 9 Object array_3 = Array.newInstance(Integer.TYPE, dims); 10 11 //获取三维数组的第3个数组组件array_2,是一个二维数组 12 Object array_2 = Array.get(array_3, 3); 13 //获取array_2的第5个数组组件array_1,是一个一维数组 14 Object array_1 = Array.get(array_2, 5); 15 //设置array_1的第7个元素的值 16 Array.set(array_1, 7, 9); 17 18 //int array[][][] = (int[][][])array_3; 19 //System.out.println(array[3][5][7]); 20 System.out.println(((int[][][])array_3)[3][5][7]); 21 } 22 23 }
用反射判断对应数组的长度,然后遍历整个数组
1 import java.lang.reflect.*; 2 public class ArrayReflectDemo { 3 4 public static void main(String[] args) { 5 String[] s1 = { "abc", "bcd" }; 6 int[] i = { 12, 34 }; 7 double[] d = { 12.34, 34.12 }; 8 ArrayReflectDemo[] af = {null,null}; 9 String[][] s2 = new String[][]{{"abc","bcd"},{"1bc","1cd"}}; 10 11 getValue(s1); 12 getValue(s2); 13 getValue(i); 14 getValue(d); 15 getValue(af); 16 17 } 18 public static void getValue(Object obj){ 19 boolean b = obj.getClass().isArray(); 20 if(b){ 21 for(int i=0;i<Array.getLength(obj);i++){ 22 System.out.println(Array.get(obj,i)); 23 } 24 } 25 } 26 } 27 ---------- 运行 ---------- 28 abc 29 bcd 30 [Ljava.lang.String;@bb0d0d 31 [Ljava.lang.String;@55e55f 32 12 33 34 34 12.34 35 34.12 36 null 37 null 38 39 输出完成 (耗时 0 秒) - 正常终止
反射实现框架功能
先建框架,然后再写类。运行时,框架调用这些类。
因为在写才程序时无法知道要被调用的类名,所以,在程序中无法直接new 某个类的实例对象了,而要用反射方式来做。这是框架要解决的核心问题。反射是一种让框架能够根据 "以字符串形式存在的信息---即配置文件" 来调用对象的属性和函数的技术,是Java对C++最大的进步之一---让框架编程真正走向平民化。
创建一个File文件config.properties作为配置文件,设置className=类名,需要时可以通过更改配置文件,将框架改造成自己需要的样子。配置文件放在工程目录下。
1 import java.io.*; 2 import java.lang.reflect.*; 3 import java.util.*; 4 public class ReflectTest1 { 5 6 public static void main(String[] args) { 7 8 try { 9 FileInputStream fis = new FileInputStream("config.properties"); 10 Properties p = new Properties(); 11 p.load(fis); 12 String cs = p.getProperty("className");//指向配置文件的key:className的value 13 /*此处配置文件设置为1或者2 14 1.className=java.util.HashSet 15 2.className=java.util.ArrayList 16 */ 17 Collection con = (Collection)Class.forName(cs).newInstance(); 18 19 fis.close(); 20 ReflectPoint1 p1 = new ReflectPoint1(1, 1); 21 ReflectPoint1 p2 = new ReflectPoint1(2, 2); 22 ReflectPoint1 p3 = new ReflectPoint1(1, 1); 23 24 con.add(p1); 25 con.add(p2); 26 con.add(p3); 27 28 Iterator it = con.iterator(); 29 while(it.hasNext()) 30 { 31 ReflectPoint1 r1=(ReflectPoint1)it.next(); 32 System.out.println(r1.getX()+"----"+r1.getY()); 33 } 34 } catch (Exception e) { 35 e.printStackTrace(); 36 } 37 } 38 } 39 class ReflectPoint1 { 40 private int x; 41 42 private int y; 43 44 public ReflectPoint1(int x, int y) { 45 super(); 46 this.x = x; 47 this.y = y; 48 } 49 50 @Override 51 public int hashCode() { 52 final int prime = 31; 53 int result = 1; 54 result = prime * result + x; 55 result = prime * result + y; 56 return result; 57 } 58 59 @Override 60 public boolean equals(Object obj) { 61 if (this == obj) 62 return true; 63 if (obj == null) 64 return false; 65 if (getClass() != obj.getClass()) 66 return false; 67 ReflectPoint1 other = (ReflectPoint1) obj; 68 if (x != other.x) 69 return false; 70 if (y != other.y) 71 return false; 72 return true; 73 } 74 public int getX() 75 { 76 return x; 77 } 78 public int getY() 79 { 80 return y; 81 } 82 } 83 配置文件1和2结果分别为: 84 ---------- 运行 ---------- 85 1----1 86 2----2 87 88 89 输出完成 (耗时 0 秒) - 正常终止 90 ---------- 运行 ---------- 91 1----1 92 2----2 93 1----1 94 95 输出完成 (耗时 0 秒) - 正常终止