笔记摘要:
这里主要介绍了java类中的反射技术,其主要是应用在框架中,这里通过介绍和反射相关的几个类:Constructor、Filed、Method类 来对它们各自
的反射方式和应用进行了说明,另外还有数组的反射,同时对HashCode和HashSet集合进行了更深层次的理解,其中的HashSet集合中出现的内
存泄露问题是值得我们注意的。
Java程序中的各个java类属于同一类事物,描述这类事物的java类名就是Class
1. Class类的各个实例对象:
对应各个类在内存中的字节码,例如Person类的字节码等
2. 字节码:
一个类被类加载器加载到内存中,占用一片存储空间,这个空间里面的内容就是类的字节码,不同的类的字节码是不同的,所以它们
在内存中的内容是不同的,这一 个个空间可以分别用一个个的对象来表示,这些对象显然具有相同的类型,这个类型就是Class类型
3. 获取各个字节码对应的实例对象(Class类型)的方法
1> 静态方法,类名.class, 例如System.class
Class cls = Person.class ;
2> 用字节码产生的对象获取 getClass(),
例如:new Date( ).getClass
3> Class.forName("类名");
这种方式可以不用知道原来的类名,只要使用的时候把类名当做参数传进去即可
4、Class.forName()返回字节码的方式有两种:
1>曾经被加载过,虚拟机中已经缓存,直接获取返回即可
2>虚拟机中还没有,用类加载器加载,然后将该字节码缓存到虚拟中,以便下次使用,同时返回刚才加载的字节码(加载)
5、九个预定义的Class实例对象:
八个基本类型boolean、byte、char、short、int、long、 float 、double加上void
6、可以用Class.isPrimitive()方法来判断是否是基本类型
包装类相应的基本类型的字节码的获取:Ingeger.TYPE == int.class
数组类型的Class实例对象:Class.isArray( );
包装类型和其基本类型不是同一份
总之,只要是在源程序中出现的类型,都有各自的Class实例对象。例如int [ ], void
public class Reflect { public static void main(String[] args) throws ClassNotFoundException { String str1 = "abc"; Class cls1 = str1.getClass(); Class cls2 = String.class; Class cls3 = Class.forName("java.lang.String"); System.out.println(cls1==cls3); //true System.out.println(cls2==cls3); //true System.out.println(cls1.isPrimitive()); //false System.out.println(int.class.isPrimitive()); //true System.out.println(int.class == Integer.class); //false System.out.println(int.class == Integer.TYPE); //true System.out.println(int[].class.isArray()); //true } }
就是把java类中的各种成分映射成相应的java类。
例如:
一个Java类中用一个Class类的对象来表示,一个类中的组成部分:成员变量,方法,构造方法,包等等信息也用一个个的Java类来表示,就像汽车是一个类,汽车中的发动机,变速箱等等也是一个个的类。
表示java类的Class类显然要提供一系列的方法来获得其中的变量,方法,构造方法, 修饰符,包,等信息,这些信息就是用相应类的实例对象来表示,它们是Filed,Method,Contructor,Package等等,一个类中的每个成员都可以用相应的反射API类的一个实例对象来表示,通过调用 Class类的方法可以得到这些实例对象
代表某个类中的一个构造方法
1、得到某个类所有的构造方法:
Constructor[ ] constructors =Class.forName("java.lang.String").getConstructors();
2、得到某一个构造方法:
通过参数来区别不同的构造函数,传入参数类型的字节码
Constructor constructor =
Class.forName("java.lang.String").getConstructor(StringBuffer.class);
//String.class.getConstructor(StringBuffer.class)
//获得方法时要用到类型
3. 创建实例对象
1> 通常方式:String str = new String(newStringBuffer("abc");
2> 反射方式:String str = (String)constructor.newInstance(new StringBuffer("abc"));
a 编译器只知道是一个构造方法,但并不知道是哪个类的构造方法,所以应该声明类型(String)
b 在获取构造方法时需指明参数类型(类的字节码)。
c 在调用方法的时候也需要传递同样类型的对象。
4、 Class.newInsrance()方法的作用:
String obj =(String)Class.forName("java.lang.String").newInstance();
该方法内部先得到默认的构造方法,然后用该构造方法创建实例对象
该方法内部的代码用到了缓存机制来保存默认构造方法的实例对象
所以如果我们使用不带参数的构造方法时用这种方法比较方便。
package cn.xushuai.test; import java.lang.reflect.Constructor; public class ConstructorTest { public static void main(String[] args) throws Exception{ //得到String类的所有构造方法 Constructor[] constructors = Class.forName("java.lang.String").getConstructors(); //得到String类中参数为StringBuffer的构造函数 Constructor constructor = Class.forName("java.lang.String").getConstructor(StringBuffer.class); System.out.println("获取String类中参数为StringBuffer的构造函数---->"+constructor); //普通方式创建实例对象 String str = new String(new StringBuffer("abcde")); //使用构造方法的的反射创建实例对象 //获取构造方法时需指明参数类型(类的字节码) Constructor constructor2 = String.class.getConstructor(StringBuffer.class); //在调用方法的时候也需要传递同样类型的对象 String str2 = (String) constructor2.newInstance(new StringBuffer("abcde")); System.out.println("普通方式创建的实例对象------>"+str); System.out.println("通过构造函数的反射创建的实例对象----->"+str2); } }
代表某个类中的一个成员变量
该示例成员变量的使用以及通过反射的方式将任意一个对象中的所有String类型的成员变量所对应的字符串内容中的"b"改成"a"。
创建一个提供反射点的类:
package cn.xushuai.test; public class ReflectPoint { private int x; public int y; public String str1 = "hahahaha"; public String str2 = "wawawawa"; public ReflectPoint(int x, int y) { super(); this.x = x; this.y = y; } @Override public String toString() { return "ReflectPoint [str1=" + str1 + ", str2=" + str2 + "]"; } }
package cn.xushuai.test; import java.lang.reflect.Field; public class FiledTest { public static void main(String[] args) throws Exception { //获取一个反射点对象并初始化 ReflectPoint pt1 = new ReflectPoint(3,6); //获得字节码的字段,Field代表类字节码的变量,不是对象身上 //的变量,要用它去取某个对象上对应的值 Field fieldY = pt1.getClass().getField("y"); //在pt1对象上取值 int num = (int) fieldY.get(pt1); System.out.println(num); //私有变量的获取 :getDeclaredField方法可见和不可见都可取到 //而getField方法只能取到可见的字段 Field fieldX = pt1.getClass().getDeclaredField("x"); //获取访问权限(暴力反射) fieldX.setAccessible(true); System.out.println(fieldX.get(pt1)); //调用反射方式修改对象的方法 changeStringValue(pt1); System.out.println(pt1); } //使用反射对某个对象的字段进行修改 private static void changeStringValue(Object obj) throws Exception { //获取所有的字段 Field[] fields = obj.getClass().getFields(); //对字段进行迭代 for(Field field : fields){ if(field.getType() == String.class){ //因为内存中使用的都是同一份字节码文件,所以用等号更合适 String oldValue = (String)field.get(obj); //将字段中的'a'换乘'z' String newValue = oldValue.replace('a', 'z'); field.set(obj,newValue); } } }
代表某个类中的一个成员方法
1、 得到类中的某一个方法:
例子: Method charAt = Class.forName("java.lang.String").getMethod("charAt",int.class);
2、 调用方法:
通常方式:System.out.println(str.charAt(1));
反射方式:System.out.println(charAt.invoke(str, 1));
• 如果传递给Method对象的invoke()方法的第一个参数为null,这有着什么样的意义呢?说明该Method对象对应的是一个静态方法!
3、 jdk1.4和jdk1.5的invoke方法的区别:
Jdk1.5:public Objectinvoke(Object obj,Object... args)
Jdk1.4:public Objectinvoke(Object obj,Object[] args),
即按jdk1.4的语法,需要将一个数组作为参数传递给invoke方法时,数组中的每个元素分别对应被调用方法中的一个参数,所以,调用charAt方法
的代码也可以用Jdk1.4改写为 charAt.invoke(“str”, new Object[]{1})形式。
package cn.xushuai.test; import java.lang.reflect.Method; public class MethodTest { public static void main(String[] args) throws Exception{ String str = "abcdefg"; //普通方式调用charAt方法 System.out.println("普通方式调用charAt---->"+str.charAt(2)); //通过反射的方式获取charAt方法 Method methodCharAt = String.class.getMethod("charAt", int.class); //将方法作用于某个对象,invoke是methodCharAt方法上的方法 char c = (char) methodCharAt.invoke(str, 2); System.out.println("反射方式调用charAt方法----->"+c); //使用JDK1.4中的invoke方法:创建一个Object数组,并在元素列表中传入一个整数,这里使用到了JDK1.5的新特性:自动装箱 System.out.println(methodCharAt.invoke(str,new Object[]{2})); } }
需求:
写一个程序,这个程序能够根据用户提供的类名,去执行该类中的main方法,因为实际开发的时候并不知道以后要执行哪个类,
而是在后期根据传入的参数去执行指定的类。
示例说明:
这个示例中是通过配置虚拟机的参数来进行演示:Run as -->run configurations-->(x)=Arguments-0>Program arguments-->填入要执行的完整类名(包括包名)
即指定要执行的类,然后获取main方法,调用invoke方法。就可执行指定类的main方法。对接收数组参数的成员方法进行反射时,传递参数的情况
JDK1.4 中数组中的每个元素对应一个参数,当把一个字符串数组作为参数传给
invoke方法时,JDK1.5 由于会兼容JDK1.4的语法,即把数组打散成若干个独立的参数所以在给main方法传递参数时
不能使用:
mainMethod.invoke(null,newString[]{"xxx","yyy","zzz"};
而要使用:
mainMethod.invoke(null,new Object[]{new String[]{"xxx"}});
mainMethod.invoke(null,(Object)new String[]{"xxx"});
编译器会作特殊处理,编译时不把参数当作数组看待,也就不会数组打散成若干个参数了
package cn.xushuai.test; import java.lang.reflect.Method; public class mainTest { public static void main(String[] args) throws Exception{ //普通方式:用静态的方式直接调用main方法 TestArguments.main(new String[]{"aaa","bbb"}); //通过反射方式调用main方法,我们需要为虚拟机配置参数:要运行的全称类名 String startingClassName = args[0]; //获取main方法 Method mainMethod= Class.forName(startingClassName).getMethod("main", String[].class); //为invoke传递参数 //mainMethod.invoke(null,new String[]{"aaa","bbb"});非法参数异常,因为javac会把它按照JDK1.4编译 //调用main方法,为了兼容JDK1.4,会拆包,将一个数组里的内容拆成3个参数,所以这里将数组打包成另外一个数组 mainMethod.invoke(null,new Object[]{new String[]{"qqq","oooo"}}); //将数组转化成Object mainMethod.invoke(null,(Object)new String[]{"qqq","oooo"}); } } class TestArguments{ public static void main(String[] args){ for(String arg : args ){ System.out.println(arg); } } }
1、 具有相同维数和元素类型的数组属于同一个类型,即具有相同的Class实例对象。
2、 代表数组的Class实例对象的getSuperClass()方法返回的父类为Object类对应的Class。
3、基本类型与Object之间的关系
基本类型的一维数组可以被当作Object类型使用,不能当作Object[]类型使用;非基本类型的一维数组,既可以当做Object类型使用,
又可以当做Object[]类型使用。
4、 Arrays.asList()方法处理int[]和String[]时的差异:
JDK1.4中:publicstatic List asList(Object[] a) 需要一个Object数组
JDK1.5中:publicstatic LIst asList(T...a) 可变参数
所以把int[ ]整体按照JDK1.5当做一个Object,将String[ ] 整体按照JDK1.4编译,当做一个Object数组,所以String[ ]可以打印出数组元素,
而int[ ],只打印出一个值。
5、 Array工具类用于完成对数组的反射操作。
该示例说明了数组与Object的关系以及其反射类型、Array工具类用于完成对数组的反射操作
package cn.xushuai.test; import java.lang.reflect.Array; import java.util.Arrays; import java.util.List; //数组与Object的关系及其反射类型 public class ArrayFlectTest { public static void main(String[] args){ int [] a1 = new int[]{1,4,3,2}; int [] a2 = new int[6]; int [][] a3 = new int[2][3];//数组里装的是数组(Object) String[] a4 = new String[]{"a","b","c"};//String是Object System.out.println(a1.getClass() == a2.getClass()); //true //编译失败:维数不同 /*System.out.println(a1.getClass() == a3.getClass()); System.out.println(a1.getClass() == a4.getClass()); System.out.println(a2.getClass() == a4.getClass());*/ System.out.println(a1.getClass().getSuperclass().getName());//java.lang.Object System.out.println(a4.getClass().getSuperclass().getName());//java.lang.Object Object aObject1 = a1; Object aObject2 = a4; //Object[] aObject3 = a1;编译失败,基本类型的一维数组不能转换成Object数组 Object[] aObject4 = a3;// a3: int [][] Object[] aObject5 = a4; //JDK1.4中:public static List asList(Object[] a) 需要一个数组 //JDK1.5中:public static LIst asList(T...a) 可变参数 //满足JDK1.4的语法,就按照1.4的编译,否则就按照1.5的编译 List list = Arrays.asList(a1); //按照JDK1.5将基本类型int的一维数组转换成list List list2 = Arrays.asList(a4); //按照JDK1.4将String类型的一维数组转换成list System.out.println(list); //[[I@160a26f] System.out.println(list2); //[a, b, c] printObj(new String[]{"haha","huhu","xixi"}); printObj("xyz"); } //反射的方式打印数组:使用Array工具类的方法通过 private static void printObj(Object obj) { Class clazz = obj.getClass(); //如果是数组,就获取每一个元素并打印 if(clazz.isArray()){ int len = Array.getLength(obj); for(int i=0;i<len;i++){ System.out.println(Array.get(obj,i)); } }else//不是数组,直接打印该对象 { System.out.println(obj); } }
1> hashCode作用:提高从几何中查找元素的效率
哈希算法的原理:
将集合分成若干个存储区域,每个对象可以计算出一个哈希码, 可以将哈希码分组,每组对应某个存储区域,根据一个对象的
哈希码就可以确定该 对象存储在哪个区域。
2> HashSet 集合的工作原理
采用哈希算法存取对象的集合,它内部采用对某个数字n进行取余的方式对 哈希码进行分组和划分对象的存储区域,HashSet集合
在比较的时候先算出对象的哈希值,找到相应的存储区,当然哈希码不同就不用比较了,若相同,然后再取出该区域的每个元素和
对象用equals方法进行比较,这样不用遍历集合中的元素就可以得到结论。可见HashSet集合具有很好的对象检索性能。
3> 什么时候覆盖HashCode方法?
如果没有覆盖HashCode方法时,是按照内存地址计算得出的Hash值比较,不同的对象的hash值当然不同,所以会被存储在不同的
区域。为了让两个相等的对象也放在相同的区域,那么如果两个对象equals 相等,那么应 该让他们的HashCode也相等,达到逻辑
上一致,这样就可以在同一个区域进行比较, 防止相等的元素重复存入,当然前提是放在哈希集合中
4> 内存泄露(无用对象占用内存)
一个对象被存储进HashSet集合以后,就不能修改这个对象中的那些参与计算哈希 值的字段了,否则对象修改后的哈希值与最初存储
的进HashSet集合中的哈希值就不同了,所以查找的不再是同一个区域这种情况下,即使在contains方法下用该对象的引用作为参数去
HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中无法单独删除当前对象,从而导致内存泄露
1 、房子与门窗的关系,房子是框架,将门窗插入框架中,框架与工具类的区别:
工具类被用户的类调用,而框架则是调用用户提供的类
2 、框架要解决的核心问题:
因为在写程序时无法知道要被调用的类名,所以在程序中无法直接new某个类的实例对象,而要用反射的方式来做。
内存泄露和反射的作用
内存泄露:
先直创建HashSet的实例对象,并对实例对象进行修改后调用remove和size方法比较两个集合的运行结果差异,来说明内存泄露。
反射的作用:
通过采用配置文件加反射的方式创建ArrayList和HashSet的实例对象,比较观察运行结果差异来说明反射的作用:实现框架功能。
必要步骤:
在src的目录下新建一个配置文件config.properties,
文件内容为:className= java.util.ArrayList.
创建一个类,提供对象
package cn.xushuai.test; public class ReflectPoint { private int x; public int y; public ReflectPoint(int x, int y) { super(); this.x = x; this.y = y; } @Override public String toString() { return "ReflectPoint [str1=" + str1 + ", str2=" + str2 + "]"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + x; result = prime * result + y; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ReflectPoint other = (ReflectPoint) obj; if (x != other.x) return false; if (y != other.y) return false; return true; } }
说明内存泄露现象并通过采用配置文件加反射的方式创建ArrayList和HashSet的实例对象
package cn.xushuai.test; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.Collection; import java.util.HashSet; import java.util.Properties; public class ReflectTest { public static void main(String[] args) throws IOException, InstantiationException, IllegalAccessException, ClassNotFoundException { //创建一个输入流并关联一个文件 InputStream in = new FileInputStream("config.properties"); //创建一个Properties对象 Properties props = new Properties(); //加载文件 props.load(in); //及时关闭关联的系统资源,否则忘记关闭时,当in被垃圾回收后,资源就无法释放了 in.close(); String className = props.getProperty("className"); //通过反射的方式获取一个实例对象(由properties文件中的className的值决定实例对象的类型) Collection collection = (Collection) Class.forName(className).newInstance(); //Collection collection = new HashSet(); ReflectPoint pt1 = new ReflectPoint(3,5); ReflectPoint pt2 = new ReflectPoint(3,7); ReflectPoint pt3 = new ReflectPoint(3,5); collection.add(pt1); collection.add(pt2); collection.add(pt3); //已经复写了hashCode和equals方法,所以无法存入 collection.add(pt1); //对象重复,无法存入 //pt1.y = 3; collection.remove(pt1); //不修改时,pt1可以被删除,size为1,修改后,无法删除,size为2 System.out.println(collection.size()); } }