黑马程序员 10 反射

------- android培训java培训、期待与您交流! ----------

反射

反射的基石-->Class类

Java程序中的各个Java类属于同一事物,描述这类事物的Java类名就是Class.

对比理解:Person类代表人,而Class类就可以代表java中用到的各种类。

Class类的对象就是每个类加载到内存后的字节码。

 

获取类的字节码的三种方法:

1.通过该类的对象的getClass()方法获取。这里该类已经被加载到内存了,因为已经有对象的存在。

2.通过类名获取。如String.class;

3.Class.forName(“java.lang.String”)方法。

 

i. 如果该类已经在内存中,则上面方法会直接返回该类的字节码。

ii. 如果该类还没有被加载到内存中,则上面方法先把该类加载到内存,然后在返回该类的字节码。


一个特殊的Class对象:Class class = void.class;

九种预定义的Class对象:其中包括8种基本数据类型对象的预定义Class对象,还包括下述的void..class对象

public static void main(String[]args)throws Exception

{

String str = "abc";

Class cls1 = str.getClass();

Class cls2 = String.class;

Class cls3 =Class.forName("java.lang.String");

System.out.println(cls1 == cls2);//true

System.out.println(cls1 == cls3);//true

}

 

这个例子说明:不管用什么方法获取类的字节码,它们都是同一个Class对象。

System.out.println(String.class.isPrimitive);//false

System.out.println(int.class.isPrimitive());//trueisPrimitive()方法判断是否是预定义类型的Class对象。

System.out.println(int.class ==Integer.class);//false

System.out.println(int.class ==Integer.TYPE);//true Integer.TYPE表示Integer中封装的基本类型

System.out.println(int[].class.isPrimitive());//false基本数据类型的数组的class对象也不是预定义的Class实例对象。

System.out.println(int[].class.isArray());//true

 

例如:一个Java类用一个Class类的对象来表示,一个类中的组成部分:成员变量,方法,构造方法,包等等信息也用一个个的Java类来表示。就像汽车是一个类,汽车中的各种组件也是一个个的类。

表示Java类Class类显然要提供一系列的方法,来获得其中的变量方法,构造器,修饰符,包等信息,这些信息就是用相应的实例对象来表示,它们是Field、

Method、Constructor/Package等。

 

一个类中的每个成员都可以用相应的反射API类的一个实例对象来表示,通过调用Class类的方法可以得到这些实例对象。得到这些实例对象后有什么用呢?怎么用呢?这正是学习和应用反射的要点。

 

Constructor类

Constructor类代表某个类中的一个构造方法

得到某个类中的所有构造方法:

Contructor constructors[] = Class.forName(“java.lang.String”).getConstructors();

得到某类中的一个构造方法:通过带参数来确定要返回的构造器

Constructor constructor =Class(“java.lang.String”).getConstructor(StringBuffer.class);

//获得带StringBuffer参数的构造方法。

获得构造方法时要用到参数类型。


创建实例对象的方式:

通常方式:Stringstr = new String(new StringBuffer(“abc”));

反射方式:String str =String.class.getConstructor(StringBuffer.class).newInstance(new  StringBuffer(“abc”));

 

调用获得的构造方法时要用到上面相同类型的实例对象。

Class.newInstance()方法

该方法内部先得到默认的构造方法,也就是不带参数的构造方法,然后用该构造方法创建实例对象

该方法内部使用了缓存机制来保存默认构造方法的实例对象。

例:String str =Class.forName(“java.lang.String”).newInstance();

Field类

   获得Field类对象的方法:

 class.getField(String name),class.getFields();//只可以获取到暴露出来的成员变量

 class.getDeclaredFiled(String name),class.getDeclaredFields();//可以得到所有成员变量,包括private。

   获得某个具体对象中的成员变量的值:field.get(objName);

package reflect;

import java.lang.reflect.Field;

public class ReflectFiedDemo

{

public static void main(String[] args)throws Exception

{

ClassDemo obj = new ClassDemo(3, 5);//x =3, y = 5;

//Field fieldY =obj.getClass().getField("y");//成员变量y是private修饰的,所以用getField方法获取不到

Field fieldY = obj.getClass().getDeclaredField("y");//这个方法可以获取包括private修饰的成员变量

//System.out.println(fieldY.get(obj));//不能直接访问对象中的private类型变量

fieldY.setAccessible(true);//暴力反射,这样就可以访问到对象中的private类型变量了

System.out.println(fieldY.get(obj));//5

Field fieldX =obj.getClass().getField("x");//变量x是公有的,所以可以直接使用getField方法获取到

System.out.println(fieldX.get(obj));//3                              也可以直接访问对象中的x变量

}

}

class ClassDemo

{

public int x;

private int y;

public ClassDemo(int x, int y)

{

this.x = x;

this.y = y;

}

}

 

Method类

得到类中的某一个方法:

Method charAt =String.class.getMethod(“charAt”, int.class);

调用方法:

通常方法:str.charAt(1);

反射方法:charAt.invoke(str,1);

如果传递给Method对象的invoke()方法的第一个参数是null,这有什么样的意义呢?  说明该Method对象对应的是一个静态方法。

Jdk1.4和jdk5.0中的区别

method.invoke(obj, Cass...arr) 5.0

method invoke(obj, Claas[] arr) 1.4

1.4中还没有出现可变参数,所以只能传一个数组。

 

用反射方式执行某个类中的main方法

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

MethodmainOfReflectDemo=ReflectDemo.class.getMethod("main",String[].class);

mainOfReflectDemo.invoke(null, newString[]{"abc"});

这样调用会出错。因为按jdk5.0的语法,javac会把数组看做一个参数,而按1.4的讲法,数组中的每个元素对应一个参数,当把一个字符串数组作为参数传递给invoke方法时,jdk5.0肯定要兼容jdk1.4的语法,会按jdk1.4的语法进行处理,即把数组打散成为若干个单独的参数。所以,在给main方法传递参数时,不能使用上面这种方式。Javac只把当作jdk1.4的语法进行理解,而不把它按jdk1.5的语法解释,因此会出现参数类型不对的问题。

自己的理解:main方法执行时应该只接收一个字符串数组类型的参数。当我们调用invoke方法传递一个字符串数组参数时,1.5的编译器是把它看做一个参数,这样是对的。但1.4的编译器是把数组里面的每一个元素当作一个参数。这样如果数组里面的元素如果不是一个,那执行时就会出现参数个数不正确的错误。而即使是里面的元素只有一个,但如果不是null而具体的值,那会出现参数类型不匹配的错误。因为执行时接收的是一个字符串数组,而传进去的是一个字符串。但如果字符串数组中的唯一元素是一个null,那执行能通过。因为null也可以代表一个null的字符串数组。

-------------------------------------------------------------------------------------------------------

 解决办法应该从两个方面来考虑:

1.要让jvm执行时只有一个参数

2.要让那个参数是字符串数组类型

 这样就得到了解决方案:

1.main.invoke(null, new Object[]{newString[]{***}});

这样编译器看到一个数组会进行折包,但折开还是得到一个字符串数组。所以传进去的还是一个字符串数组。

2.main.invoke(null, (Object)newString[]{***});

这样编译器看到的只有一个元素,就不会再折包,传进去的虽然封装成一个Object对象,但本质是一个字符串数组,执行时强转后还是能得到一个字符串对象。

 

问题:既然我们可以使用类名直接调用静态方法的特点来调用main方法,为什么还要有到反射?

因为我们并不是能一直认识要调用的类,也就是要调用的类的类名是不确定的。比如当我们使用框架时,框架如果知道要调用自已写的类的类名。因为原先是没有这个类的,而如果使用反射,我们只用把要调用的自己的类的类名当作参数传递给框架,框架就能通过反射来调用该类。也许你会问,因为一开始我也问了。都把类名传递过来了,那直接通过类名调用静态方法不就可以了吗。但是,我忽略了一个很简单的问题,传递过来的只是一个字符串!!不可以直接通过类名的字符串调用类的方法吧…


 

数组的反射

具有相同类型元素和相同维度的数组指向同一个Class对象。

int[] a1 = new int[3];

int[] a2 = new int[4];

int[][] a3 = new int[1][2];

int[][] a4 = new int[2][5];

String[][] a5 = new String[3][4];

System.out.println(a1.getClass() ==a2.getClass());//true

System.out.println(a4.getClass().equals(a3.getClass()));//true

System.out.println(a1.getClass().equals(a3.getClass()));//false

System.out.println(a1.getClass().equals(a5.getClass()));//false

System.out.println(a1.getClass().getSuperclass().getName());

System.out.println(a3.getClass().getSuperclass().getName());

System.out.println(a5.getClass().getSuperclass().getName());

结果是:java.lang.Object

 java.lang.Object

 java.lang.Object:

由此可以看出:数组的父类都是Object为。

所以有

Object obj1 = a1;//正确:数组的父类就是Object类

Object obj2 = a3;//正确:数组的父类就是Object类,不管几维

Object obj3 = a5;//正确:数组的父类就是Object类,不管类型是什么

Object[] obj4 = a3;//正确:理解成一维的Object数组里面装了一维int类型的的Object

Object[] obj5 = a5;//正确:理解成把一维的字符串数组强转为一维的Object数组,因为String类型的父类也是Object

Object[] obj6 = a1;//错误:因为a1是int类型数组也是一个Object,但不是一个Object的数组。

上面的现像说明了一个小问题:

 

当我们想把数组中的元素直接显示出来时,直接输出的话显示的是对象的名称,而不能显示出里面的元素来。这时我们就想到了List重写了toString方法,可以把列表中的元素显示出来。所以我们想把数组转换成list来打印。这就用到了Arrays工具类中的asList方法,可以把数组转换成列表。而当我们调用asList方法把int[]数组和String[]数组转换成list时,直接打印后String[]数组把显示出各个元素,而int[]数组不能,还是显示对象名。这是为什么呢? 因为jdk1.4中,asList方法的参数是Object[],而String[]能转换成Object[],但int[]不能转换成Object[]。所以当String[]转换成list时,会把String[]中的每个元素当成列表中的一个元素。而会把int[]看作是一个单独的Object对象,这里jdk1.4不能识别,就找jdk1.5的asList,而jdk1.5的参数是T...arg,它是一个泛型的可变参数,这里会把int[]看作是一个Object对象,然后当作一个Object对象参数调用asList方法,这时因为就只有一个Ojbect对象参数,所以里面还是只有一个元素。


hashCode和equals的作用

当我们使用Object类中的equasl方法判断两个对象是否相同是,其实本质执类的两个对象只要它们的属性相同就是同一对象,则可以重写这个类的equals方法进行判断。这样当调用equals方法判断两个对象是否相同时,只要它们的属性值相同,就能返回true; 但是这里有一个问题!对象在Hash类型的集合中存取的问题。要搞懂这个问题首先需要了解Hash类型的集合的存取道理。

 

要知道Hash类型的集合存储时是调用对象的equals方法进行对比,但是如果每存储一个就要和集合中的所有元素依次进行对比才能知道该对象能否存入集合中,这样做效率很低,因为当集合中的对象很多时,比较的次数会很大,存储就很慢。这时就有人发明了一种哈希算法来提高从集合中查找元素的效率。这种方式是把集合分成若干个存储区间,然后根据一定算法让每个对象算出一个哈希值,然后根据哈希值进行分组存储,每个组对应不同的存储区间。这样,当一个对象要存进来时,只要算出它的哈希值,然后在该哈希值对象的区间中遍历是否有和该对象equals的对象即可。

明后上面的道理后就可以引出我们的问题。如果当我们重写了equals方法后,虽然能自己判断两个对象相等的条件,但是如果两个相同的对象在存储到Hash类型的集合中时,由于哈希值不同,分配到的存储区间也可能不同,就找不到相同的那个对象。这样就会出现虽然这两个对象按我们的要求应该是两个相同对象,不能存储到同一个hash集合中,但实际情况是它们的的确确是放在了同一个hash集合中了。所以java规定,一般情况下,如果重写了equals方法,那hashCode方法也要重写。而且一般重写都是根据属性来求哈希值的。这样如果两个对象的属性值相同,那计算得到的哈希值也相同,就不会出现上述这样的问题了.

还有一点值得我们注意的是:如果对象已经存入到hash集合中了,不是必要时别改变这些对象的值。否则会出现一个问题:内存泄漏。比如在一个对象存入到集合中后,假如你对该对象的属性值进行改变,那这个对象在集合中的存储空间可能发生改变,因为它的哈希值是根据属性值计算的。然后当我们用不到这个对象了,要删除该对象的时候(如collection.remove(obj)),会在原来的存储区间中找该对象,而这时对象已经不在这里。所以删除就失败了,而我们并不知道该对象没有被删除掉。所以就导致了内存泄漏,而且还可能导致程序错误,因为自己是以为该对象已经被删除,但事实上还在集合中存在。

内存泄漏就是不需要的对象依然在内存中存在,没有释放掉空间,一直占用着内存。


Java中不仅有对象会导致内存泄漏,资源也会导致内存泄漏。比如当使用流资源时,如果使用完后不把流对象的资源进行close释放,那即使这个流对象被gc回收后,流资源也可能不会被释放。这样就导致了内存泄漏。

 

总而言之, Java虽然有自动回收管理内存的功能,但内存泄漏也是不容忽视,它往往是破坏系统稳定性的重要因素

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值