黑马程序员——反射

来源:互联网 发布:javascript submit 编辑:程序博客网 时间:2024/05/21 20:22

------<ahref="http://www.itheima.com" target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流! -------

 

反射虽然是从Java1.2开始的,但是其实是Java中比较难的高级技术,没法像上面所学的知识那样一上来就提纲挈领地介绍,需要我们由浅入深地慢慢学习。

首先我们要学习使用反射,需要导入包import java.lang.reflect.*;不要想当然的一位是lang包里的内容就不用导了,刚开始我自己学的时候怎么编译怎么错,一个字母一个字母查的,折腾半天发现是这个包的问题。

Class类,【注意】不是class关键词。

我们知道Java中的类用于描述一类事物的共性,该类事物应该具备什么样的属性,如Person类描述人,InetAddress类描述IP地址,Excetpion类描述异常,Thread类描述线程,复杂事物有多少,相应的类就有多少,那么这些类本身也是一类事物,也应该有一定的共性,那么就用Class类来描述。

通过查阅JDK的文档我们发现Class类中有很多方法能帮我们获取到每一个类中所具备的信息,如String getName()  获取类名,Method[]getMethods()  获取方法,Class<?>[]  getInterfaces()  获取实现的多个接口,PackagegetPackage() 获取此类的包 Field getField(String name)  获取成员变量 等。

Class的实例对象,代表内存里的一份字节码,是不能new出来的。(没有提供构造方法)

字节码概念:

我们写的代码编译过后生成.class的二进制代码(在硬盘上)如Person.class,当我们运行程序的时候,源程序里用到Person这个类的时候,虚拟机会先将硬盘上的这些二进制代码加载到内存里面,称为字节码文件(可以理解为内存里的class文件),然后接着才可以用它去创建(复制)一个个对象,程序里面不管创建了多少对象,只看用到了几种类创建对象,内存里面就有几份字节码,每一份字节码,都是一个Class的实例对象,这个对象的类型就是Class。因此我们可以用Class变量指向这些实例对象。如Class cls1 = Person.class

 

得到Class类型的实例对象(字节码对应的实例对象)有三种方式:

1.      类名.class,例如System.class//固定写法没有为什么。

2.      对象.getClass(),例如new Person().getClass();//每个对象理应能找到创建自己的字节码文件。所有类都有这个方法,从Object上帝那里继承来的。

3.      Class.forName(“类名”);静态方法,写法如Class.forName(“java.util.Date”);参数里面必须填完整类名。

做反射的时候主要用第三种,因为在写源程序的时候可能还不能确定,就可以通过后期传一个字符串,并用这个字符串作为这个方法的参数。这个方法是静态方法,它有两种情况:一个是目标为已经加载进内存中的字节码,直接找到并返回。第二个是还没有进内存的类,它会强行让这个类加载进虚拟机,并返回它所对应的字节码。

但不管用上述哪种方式,虚拟机里面只会有一份字节码,如果已经存在的话会直接获取它并返回,不会再建立新的。

八个基本数据类型(byte boolean int float double short long char)也有对应的Class对象。返回值类型void也有对应的Class对象。所以一共有9个预定义的Class实例对象。

Class里面还有isPrimitive()方法,能判断是否为预定义Class实例对象。

每个基本类型的包装类,如Void, Integer等等都有一个常量TYPE,代表它们所包装的基本类型的字节码,如Integer.TYPE==int.class。

数组也是一种类型,但不是上面那种预定义的类(我们之前学过数组是对象,在堆内存里),就算它里面装的是基本数据类型。

而Class对象还有一个方法能判断是否为数组:isArray()

总之,只要是在源程序中出现的类型,都有各自的Class实例对象,例如int[], void…

 

反射的核心

就是把Java类中的各种成分映射成相应的Java类,从而进行一系列高级操作。如获取到的成员变量就是一类Field,其他比较常见的类如Method获取类中的方法,Constructor获取构造函数,Package获取包等等。

Constructor的反射

getConstructor(Class<?>...parameterTypes)  方法获得类的一个构造函数,思考:一个类有很多构造函数,如何选择具体哪一个呢?我们知道每个构造函数都不一样,通过他们的传入参数来区分,所以这个方法里面的传入的参数就是那个类型的类类型对象。而且该方法的传入参数是一个可变参数(1.5后新特性),可以传入若干个Class对象,每一个Class对象代表构造函数的参数的一种类型。

在可变参数出现之前的做法是传入一个数组,里面放着那些类类型对象,数组里面装几个就是几个

1.5也兼容1.4的数组参数,一般我们用Object类的数组Object[]来作为参数传入,该数组里面也可以放int等基本数据类型,因为会自动装箱为Integer对象。

但注意Object[]不接受int[](即Object[] a= new int[3];),因为int数组里面的类型是死的就是int型,不能自动装箱为Integer,如果这么写会发生IllegalArgumentException非法参数异常,而可以接受Integer[]数组,可以这么理解:这个方法的参数规定是一个数组,只要数组里面的实际内容是对象就可以,Integer[]数组里面装的都是Integer对象,而int数组里面装的是绝对不可能变成对象的。另外有一点区别的是Object[]可以接受一个Object[]数组对象,里面装着int[]数组,因为int数组是对象,它的父类就是Object。代码:Object[] a = new Object[]{new int[3]};

总而言之,为避免各种问题,就用Object[]数组装就好了,里面能接收任意类型的参数)

如果原构造函数第一个是int类型,这个方法的传入参数就是int.class。

获得了构造方法的对象之后,我们可以用Constructor里面的newInstance方法创建新对象出来。我们之前获得了哪种构造函数,在新建对象时就要传入哪种构造函数的实际参数。

一句话:获得方法时要用到类型,调用获得的方法时要用到上面相同类型的实例对象(利用获得的构造函数类的newInstance方法创建对象)。

这里注意:编译时虚拟机并不知道我们用的是哪种构造方法,也不知道这个构造方法属于谁,只知道我们要用一个Class对象获取出来的构造方法对象新建一个对象,运行时才能明确这些,因此瞎传一个参数编译也不会报错,而运行会报异常。为了避免安全提示,我们可以加入泛型来明确构造方法属于谁,如果没加泛型newInstance的返回类型是Object,需要强转。

通过读该方法的源代码可以发现,在建立对象的时候函数会建立一个缓存把取到的数据存起来,说明反射的速度对于计算机来说比较慢。

【附】看Java源代码的方法:在C:\Program Files\Java\jdk1.6.0_43目录下有个src.zip的压缩包,打开后里面找到java/lang/Class.java文件打开即可。

如果需要用不带参数的构造函数新建一个对象,也可以直接使用Class类的newInstance()方法。

Field的反射

代表字节码的变量的类型,一个Field对象就是一个字节码的一个变量类型的一个对象。而不代表一个具体的对象的变量。换言之,Field的对象是一个类里面的变量的对象,而不是一个对象里的变量。

getField(“Stringname”) 方法具体获得该类哪个成员变量的对象,用那个成员变量的名字来确定。

【再次注意】取出来的内容不是对象身上的变量,而是那个变量类型的对象,要用它去取某个对象上对应的值。

获得完Field对象后,可以用里面的Objectget(Object obj)方法获得这个变量具体在obj这个对象里面对应的值。注意返回类型是Object需要强制转型。

getField方法只能获得可见的成员变量的对象,对于那些私有的如果用此方法会返回NoSuchFieldException异常【看不到】。

我们可以用getDeclaredField(Stringname)方法获取到,只要声明过的不管变量是否私有都行。【看到了】现在仅仅是能获得到那个成员变量的对象了,但是get方法获得这个成员变量在某个对象中的具体值对于private的变量还是做不到,会报IllegalAccessException异常,【拿不到】这个时候就要用到Field中的setAccessible(true)方法把它设置成可以访问的就能通过get方法拿到数据了。(这就叫做暴力反射)【拿得到】

 

反射小应用:

把某个对象里的所有String类型的变量的a换成b。(视频里是b换成a)

字节码对象与字节码对象之间比较都用==比而不用equals比(虽然也可以,因为底层用的也是==,但是语义不准确),因为同一种类型如String在内存里只有一份字节码文件。

Field的getType()方法返回的是那个成员变量的Class类的对象(字节码对象)。

用String的replace(charoldChar, char newChar);方法可以替换所有匹配字符。

用Field的voidset(Object obj, Object value)方法来设置某个对象的变量值。(哪个变量不用指定,看Field具体是哪个对象)

方法代码如下:

private static void changeStringValue(Object obj) throws Exception//该方法接收一个对象进来{Field[] fields = obj.getClass().getFields();//取出对象中所有变量的类类型对象。for (Field field : fields)//通过高级for循环遍历出每一个Field{if (field.getType() == String.class)//判断该变量的对象是否为我们所需要的String类的对象{String oldValue = (String)field.get(obj);String newValue = oldValue.replace('a','b');//改值field.set(obj,newValue);//赋新值}}}

Method的反射

是类里面的方法,而不是对象里面的方法。

获得到某个类的对象后,用它里面的getMethod方法获得其方法的对象。

getMehtod方法有两个参数,Stringname, Class<?>... parameterTypes 前者是方法名,后者是该方法的参数(可变参数类型)的类对象,因为会有同名方法的存在(重载)。

得到这个Method对象后,再用其invoke(Objectobj, Object... args)方法,(两个参数,前面的是某个具体的拥有该方法的对象,后者是给这个方法的传参,可变参数)调用某个对象的这个方法。而静态方法不需要创建对象,可以在第一个参数里面填入null。

Method的反射练习:

写一个程序,这个程序能够根据用户提供的类名,去执行该类中的main方法。

         假设我们的一个类是这样的,它的main方法只有最简单的打印传入的参数的功能:

class TestArguments{public static void main(String[] args){for (String arg : args){System.out.println(arg);}}}

那么我们要想执行该类的main方法步,我们首先想到的是学反射之前最简单的做法:因为main是静态方法,故直接调用并传参数进去即可。

TestArguments.main(new String[]{"aaa","bbb","ccc"});//传统方式:在程序里面用静态代码的方式直接调用main方法。

但是如果要依靠反射的知识来完成的话,步骤如下:

1.        首先要获得该类的Class类型对象。

2.        通过该对象获得其main方法的对象。

3.        通过该对象的invoke方法运行这个main函数。

String startingClassName = args[0];//需要我们在启动主函数的时候输入要操作的类名TestArgumentsMethod mainMethod = Class.forName(startingClassName).getMethod("main",String[].class);//将1、2两步合二为一成一句话。//【注意】,下方高能,比较困难的来了。mainMethod.invoke(null,new Object[]{new String[]{"aaa","bbb","ccc"}});//因为是静态invoke方法第一个参数是null,invoke方法的第二个参数在JDK1.5里面是可变参数,在1.5之前是一个存着那些参数的数组,而JDK1.5向下兼容,会先判断是否为数组,如果是则会自动对传入进来的数组拆包,我们想传一个String的数组,里面装着3个String对象,想让他们分别打印,可是这个数组会被拆成3个String对象,因此我们索性先把String数组装进一个Object数组里面,因为拆包只拆一次。//替代方法mainMethod.invoke(null,(Object)new String[]{"aaa","bbb","ccc"});//只要我们传入的不是数组就不会拆包,那么就在String[]数组前面加上(Object)给自动类型提升一下,就不会拆包了。

上面这个例子有个麻烦的地方:invoke方法(Objectinvoke(Object obj, Object... args))后面那个参数是可变参数,在JDK1.5之前要传入一个由那些参数所组成的数组,而这个数组被当做是多个参数处理。而1.5仍然向下兼容这种传入参数形式,而Java会判断后一个参数是否为数组,是的话自动把这个数组里的参数拆包取出来。所以我们设计的程序如果传入一个数组new String[] {“a”, “b”, “c”}把它发给主函数并想调用目标类的主函数让里面的元素分别打印的话,invoke方法会把这个数组里面的具体内容一个个拿出来当成对象,也就是相当于三个String,可是主函数本来应该接收的参数是一个String[]类型的数组,会发生IllegalArgumentException异常。

而如果用传统的方法调用main函数再传入一个String类型的数组就不会有问题,因为不管是1.5之前还是之后main函数接收且只能接收String类型的数组,匹配。

解决方案:

1.      我们可以在这个String[]数组上先封装一下包,把这个数组放在一个Object数组的第一项(所有数组都是Object的子类。数组也是对象),这样main在接收到这个数组的时候会先拆包,但只会拆一次,把String[]拿出来当做自己的参数,这正是我们需要的。

2.      或者换一种思路,只要我们传入的不是数组就不会拆包,那么就在String[]数组前面加上(Object)给自动类型提升一下,就不会拆包了。

数组的反射

具有相同的元素类型以及具有相同的维度(一维数组二维数组。。。)的数组的Class都是同一个字节码文件。

数组的命名规则:

一维int数组:[I

二维int数组:[[I

一维String数组:[Ljava.lang.String;

一维Object数组:[Ljava.lang.Object;

基本上就是[*维度+它元素所属的类的字节码文件的getName()方法

 

Arrays的asList方法的参数:1.5支持可变参数T…a,任意个对象。1.4支持Object[] a 一个存储着各种参数的数组。当我们传入的参数是String[]时,走的是后者,因为数组内每一个String都是对象。

而如果传入的参数是int[]时,走的是前者,因为int[]里面的每个int都不能自动装箱为Integer,所以都是基本数据类型而不是对象,只有int[]本身是对象,所以就当做一个普通对象处理。

 

数组的反射练习:

写一个打印程序,如果是普通的对象,直接打印,如果是数组,就把数组里每一个元素打印出来。

这里面要用到Array这个类(属于反射类要导包),这个类与util里的Arrays相对应都是对数组的操作的工具类,不同的是Array里的方法专门为反射设计。

方法部分代码如下:

private static void printObject(Object obj){Class cls = obj.getClass();if (cls.isArray()){int len = Array.getLength(obj);int x=0;while(x<len-1)System.out.print(Array.get(obj,x++));System.out.println(Array.get(obj,x));}else{System.out.println(obj);}} 

【注意】无法得到整个数组的元素类型,只能取出其中一个元素,然后用getClass()方法知道那个元素的类型,但是Object类可以装各种类型的元素。

框架

所谓框架就是预定义一个系统出来,接收未来传入的对象。扩展性极高。

框架就类似于一个毛坯房,在这个已经是半成品的基础上让框架调用自己创造的各种元素生成最后的产品,更加优越,更加快速。

就像我们写作文用的模板,整个文章的框架已经出来了,我们换个人名,换个时间地点就成了我们自己的文章了(夸张的说法)。

框架需要能够调用以后产生的类。

我们把框架的关键信息存储在一个properties文档(以键值对的形式存在)里面,这样用户不用修改代码,直接修改这个文档就可以完成对程序的修改。

 

练习:用反射和框架的方法以及思想修改HashSet集合那个例子。

代码如下:

import java.util.*;import java.io.*;class ReflectTest3 {public static void main(String[] args) throws Exception{InputStream ips = new FileInputStream("config.properties");//从外部加载配置信息作为我们这里调用的类Properties props = new Properties();props.load(ips);ips.close();String className = props.getProperty("className");Collection collections = (Collection)Class.forName(className).newInstance();//框架方法,注意forName里面的参数得是完整类名,也就说是要包括包名。//Collection collections = new HashSet();非框架方法ReflectPiont pt1 = new ReflectPiont(3,3);ReflectPiont pt2 = new ReflectPiont(5,5);ReflectPiont pt3 = new ReflectPiont(3,3);collections.add(pt1);collections.add(pt2);collections.add(pt3);collections.add(pt1);sop(collections.size());}public static void sop(Object obj){System.out.println(obj);}}class ReflectPiont {private int x;public int y;ReflectPiont(int x, int y){this.x = x;this.y = y;}public int hashCode(){return x*7+y*13;}public boolean equals(Object obj){return this.hashCode()==obj.hashCode();}}

hashCode的实现原理:(拔高)

一个元素要进集合,具体分为如下几部:首先算出它的hashCode,然后用hashCode取模,得出结果后,根据那个结果放到HashSet的指定区域中(比如说拿32取模,那么结果就有32种,就分了32个区)再来一个新的元素不用每个元素挨个比了,直接算出hashCode取模的结果,然后根据这个结果上指定的区域寻找,再在那个确定了的区域里和其中的元素挨个比,这样相当于缩小了32倍的查找范围能提高效率。

 

还有一点非常重要注意:

当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算hashCode值的字段了。否则,对象修改后的哈希值与最初存储进HashSet集合中的哈希值就不同了,这种情况下检索对象返回不出对象的结果,这会导致无法从HashSet集合中单独删除当前对象(要删除肯定先得找到,可是我们存储的时候是按照A哈希值存进去的,集合因此把那个哈希值与那个对象关联起来,可是当我们修改了参与哈希值运算的参数的时候,对象的哈希值就变化了,而这个变化仅仅在对象上体现,没有反映到集合中,这时我们再拿着这个对象去查找的话系统会根据新算出的哈希值去查集合中过时的哈希表,肯定是找不到对应的哈希值,因此就相当于没有找到那个对象,所以删不了),从而造成内存泄露。(有的资源以后都再也不会被使用了,可程序往下运行,它还没有被释放掉,浪费内存)。

与hashCode有关的集合才需要考虑上面的,否则如果像是ArrayList跟这个没关系。

 

上面我们可以在主函数里加这样一行代码

pt1.y = 7;

这样参与哈希运算的参数就改变了,哈希值也会跟着改变。

再试试看能否在集合中删除这个元素。

collections.remove(pt1);sop(collections.size());

发现删除失败,集合的长度没有变化,内存泄露!




0 0