黑马程序员:反射的介绍、hashCode引起的内存泄漏讲解

---------------------- ASP.Net+Android+IOS开发.Net培训、期待与您交流! ----------------------

反射 (会导致程序性能下降,但对未知类的扩展很有用)

反射的基石->Class类
Java类用于描述一类事物的共性,该类事物有什么属性,没有什么属性,至于这个属性的值是什么,
则是由这个类的实例对象来确定的,不同实例对象有不同的属性值
Java程序中的各个Java类,它们是否属于同一类事物,是不是可以用一个类来描述这类事物呢?答案肯定是可以的,
这个类的名字就是Class,要注意和小写class关键字的区别。
Class类描述了哪些信息呢?
类的名字,类的访问属性,类所属的包名,字段名称的列表,方法名称的列表等等
Method[] getMethods() 
String getName() 
Package getPackage() 
boolean isInterface() 
Person p1 = new Person();
Person p2 = new Person();
Class cls1 = Person.class; //字节码1
Class cls2 = 字节码2;
  
对比提问:Person类代表人,它的实例对象就是张三、李四这样一个个具体的人,
Class类代表Java类,它的各个实例对象又分别对应什么呢?
对应各个类在内存中的字节码,例如Person类字节码,ArrayList类的字节码等等
一个类被类加载器记载到内存中,占用一片存储空间,这个空间里面的内容就是类的字节码,
不同的类的字节码是不同的,所以它们在内存中的内容是不同的,这一个个的空间可分别用一个个的对象
来表示,这些对象显然具有相同的类型,这个类型是什么呢?Class类型
  
如何得到各个字节码对应的实例对象(Class类型),
一个字节码可以创建多个对象,每个类型都有自己的Class实例对象
1.类名.class 例如:System.class
2.对象.getClass() 例如:new Date().getClass()
3.Class.forName("类名") 例如:Class.forName("java.util.Date"); //反射的时候用这种方式
其中第三种获取方式为,若需要获取的类在内存中存在,则直接返回字节码对应的实例对象,
若内存中不存在,则JVM先通过类加载器加载该类,再返回字节码对应的实例对象
  
九个预定义Class实例对象:
八个基本数据类型(int.class ...)和void(void.class)
int.class == Integer.TYPE  //TYPE这个常量代表的是包装类型所包装基本类型的字节码(表示基本类型 int 的 Class 实例)
void.class == Void.TYPE
  
判断是否是数组类型的Class实例对象
Class.isArray();
示例:System.out.println(int[].class.isArray()); //true


String str1 = "absssssc";
String str2 = "abcqwe";
String str3 = new String("asdfcd");
Class cls1 = str1.getClass();
Class cls2 = String.class;
Class cls3 = Class.forName("java.lang.String");
System.out.println(str1.getClass()==str2.getClass());//true
System.out.println(str1.getClass()==str3.getClass());//true
System.out.println(cls1.isPrimitive()); //false String不是基本类型字节码
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.isPrimitive()); //false 数组不是原始类型
System.out.println(int[].class.isArray()); //true


反射就是把Java类中的各种成份映射成相应的Java类
例如,一个Java类中用一个Class类的对象来表示,一个类中的组成部分:
成员变量,方法,构造方法,包等等信息也用一个个的Java类来表示,
就像汽车是一个类,汽车中的发动机,变速箱等等也是一个个的类,
表示Java类的Class类显然要提供一系列的方法,来获得其中的变量,方法,
构造方法,修饰符,包等信息,这写信息就是用相应类的实例对象来表示,它们
是Field,Method,Contructor,Package等等
比如:
System.exit
System.getProperties();
以上都是方法,那么就有两个对象methodObj1,methodObj2,他们的类型就是Method(方法)
Method --> methodObj1(对应System.exit), methodObj2(对应System.getProperties();)


Constructor类 (构造方法类)
Constructor类代表某个类中的一个构造方法
得到某个类所有的构造方法:
Constructor[] constructor = 
Class.forName("java.lang.String").getConstructors();
得到某一个构造方法:
Constructor constructor = 
Class.forName("java.lang.String").getContructor(StringBuffer.class);
//获得构造方法要用到类型,这里是StringBuffer类型
创建实例对象:
//调用获得的方法时要用到上面相同类型的实例对象,这个例子中new StringBuffer()
普通的方法:String str = new String(new StringBuffer("abc"));
反射的方法:String str = (String)constructor.newInstance(new StringBuffer("abc"));
System.out.println(str.charAt(2));


Class.newInstance():
例子:String str = (String)Class.forName("java.lang.String").newInstance();
该方法内部得到默认的构造方法,然后用该构造方法创建实例对象
该方法内部代码中用到了缓存机制来保存默认构造方法的实例对象
与通过构造方法创建实例对象相比,这样更简单了,当然也有局限性,即只能创建默认构造函数的实例对象
通过构造方法创建实例对象的路径:
class-->constructor-->object
而这个方法的路径:
class-->object


Field:代表字节码中的变量,不代表某个对象中的变量
获得字节码(ReflectPoint.class)中的变量public x,private y
ReflectPoint rp1 = new ReflectPoint();


//public x
Field fieldx = rp1.getClass().getField("x");//获取rp1对象对应的字节码文件中的x变量,fieldx不代表具体值
System.out.println(fieldx.get("rp1"));//获取rp1对象的x变量的值


//private y
Field fieldy = rp1.getClass().getDeclaredField("y"); //获取rp1对象对应的字节码文件中的y变量,只要声明过的就可以获取到,不管私有还是公有,而getField()只能获取公共的
fieldy.setAccessible(true);//设置fieldy可以获取某个对象中y变量的具体值(因为y是私有的,外部调用没有权限,所以一定要先setAccessible(true),让其有访问权限)
System.out.println(fieldy.get("rp1"));//获取rp1对象的y变量的值


练习题:将任意一个对象中的所有String类型的成员变量所对应的字符串内容中的"b"改成"a"
Field[] fields = rp1.getClass().getFields(); //获取rp1对象所对应的字节码文件中的所有公共字段(公有成员变量)
for(Field field : fields) {
	if(field.getType() == String.class) { //getType()返回变量类型,因为字节码文件只有一份,所以可以用==比较
		String oldValue = (String)field.get(rp1); //获取rp1对象String类型变量的值
		String newValue = oldValue.replace('b','a'); //将值中所有b换成a
		field.set(rp1,newValue); //给rp1对象field所对应的变量设置新值
	}
}

Method类:代表某个类中的一个成员方法
得到类中的某个方法:
String str1 = "abc";
Method methodcharAt = Class.forName("java.lang.String").getMethod("charAt",int.class);
反射的方法:
System.out.println(methodcharAt.invoke(str1,1)); //b
普通的方法:
System.out.println(str1.charAt(1)); //b

若传递给Method的对象的invoke()方法的第一个参数为null,这就说明Method对象对应的是一个静态方法
JDK1.5和JDK1.4的区别:
JDK1.4:
public Object invoke(Object obj, Object[] args)
System.out.println(methodcharAt.invoke(str1,new Object[]{2})); //c


JDK1.5:
public Object invoke(Object obj, Object... args)
System.out.println(methodcharAt.invoke(str1,1)); //b


需求:写一个程序,这个程序能够根据用户提供的类名,去执行该类中的main方法
问题:
启动Java程序的main方法的 参数是一个字符串数组,即public static void main(String[] args),通过反射的方法来调用这个main方法时,如何为invoke方法传递参数呢?按1.5的语法,整个数组是一个参数,而按JDK1.4的语法,数组中的每个元素对应一个参数,当把这个字符串数组作为参数传递给invoke时,javac到底会按哪个语法进行处理呢?JDK1.5肯定要兼容jdk1.4的语法,所以肯定按jdk1.4的语法进行处理,即把数组拆分为若干个单独的参数,所以,在给main方法传递参数时, 不能使用代码 xx.invoke(null,new String[]{"a","b"}),javac只把它当作jdk1.4的语法进行理解,而不把它当作jdk1.5的语法解释,因此 会出现参数类型不对的问题
解决办法:
1.xxx.invoke(null,new Object[]{new String[]{"a","b"}}); //javac会把Object数组中String数组当成一个参数传入
2.xx.invoke(null,(Object)new String[]{"a","b"}); //javac会把String[]数组认为是一个Object对象,自然就不会按数组拆分了


Java反射处理数组和可变参数:
Method mainMethod = clazz.getMethod("main", String[].class); 
/* 写法一:
 * 创建一个具有指定的组件类型和长度的新数组,且返回值为Object类型。
 * 在创建的数组中添加元素
 * 最后invoke
 */
Object arrayObj = Array.newInstance(String.class, 2);  
Array.set(arrayObj, 0, "Hello");  
Array.set(arrayObj, 1, "World");  
mainMethod.invoke(object, arrayObj); 

// 写法二  
mainMethod.invoke(object, new Object[]{new String[]{"Cafe","Baby"}}); 
// 写法三
mainMethod.invoke(object, (Object)new String[]{"Cafe","Baby"}); 




数组与Object的关系及其反射类型
数组的字节码:
当类型和维度都相同的数组属于同一个类,即他们具有相同的Class实例对象
例如: int[] a1 = new int[3];
int[] a2 = new int[4];
System.out.println(a1.getClass() == a2.getClass()) //true


一维数组可以当作Object类型使用,但不能当作Object[]类型使用。
非基本类型的一维数组,既可以当作Object类型使用,又可以当作Object[]类型使用,如String[] str = new String[3];
获得数组字节码的父类字节码:
int[] a1 = new int[3];
int[][] a2 = new int[3][2];
a1.getClass().getSuperclass().getName(); //java.lang.Object
a2.getClass().getSuperclass().getName(); //java.lang.Object
结论:不管几维数组,其Class对象的父类Class对象都是Object


所以:
int[] a1 = new int[3];
int[][] a2 = new int[3][2];
String[] str = new String[3];


Object obj0 = a1; //这可以
Object[] obj1 = a1; //这是不行的   因为这样写的花,就相当于Object数组中装有int类型的元素,而Object数组必定装的是Object类型或其子类
Object[] obj2 = a2; //这可以 Object数组中装的是多个 一维数组 a2[]
OBject[] obj3 = str; //这可以 String是Object的子类


Arrays.asList()方法处理int[]和String[]时的差异:
String[] 被认为是Object[]类型,所以按JDK1.4中的asList(Object[] obj)方式传入;而int[]被认为是Object类型,所以按JDK1.5中的asList(Object... obj)方式传入


Array工具类用于完成对数组的反射操作
思考:怎么得到数组中的元素类型?
答案:想要获取数组中的元素类型,只能获取数组中具体值的数据类型,因为Object[]中有很多类型的数组,可以有int[],String,等等。
Object[] obj = new Object[]{"b",1};//这里的1会被自动装箱,是Integer类型
obj[0].getClass().getName();获取b的类型

数组反射的应用:
public static void main(String[] args) throws Exception{
String[] a1 = new String[]{"aaaa","bbb","ccc"};
String a2 = "xyz";
printObject(a1);
printObject(a2);
}

private static void printObject(Object obj) {
	// TODO Auto-generated method stub
	if(obj.getClass().isArray()) {
		int len = Array.getLength(obj);
		for(int x=0; x<len; x++){
			System.out.println(Array.get(obj,x));
		}
	}else{
		System.out.println(obj);
	}
}

hashCode引起的内存泄露(一个对象已经不用了,但是却一直占用一块内存空间):
现有Demo类,Demo类中覆盖了hashCode方法,以Demo类中变量a的值判断hashCode是否相同。
当底层是哈希表数据结构的集合存入一部分Demo对象后,若修改了a变量的值,这时候集合的remove方法
就不会删除对应的对象,因为hashCode值在改变a变量的时候随之改变了,最初存入集合的hashCode值和现在的值不一样了,
所以remove方法找不到对应Demo对象的内存地址,contains方法也找不到对应的元素,
长久下去,导致集合中很多元素并未删除,又不断的增加,就会导致内存溢出。
故:当对象存入底层为哈希表数据结构的集合后,就不能修改该对象中参与hashCode运算的变量值,这样就可以防止内存泄露
代码演示:
public class Test {

	public static void main(String[] args) throws Exception{
	Collection al = new HashSet();
	Demo d1 = new Demo(1);
	Demo d2 = new Demo(2);
	Demo d3 = new Demo(3);
	Demo d4 = new Demo(4);
	al.add(d1);
	al.add(d2);
	al.add(d3);
	al.add(d4);
	al.add(d1); //这里重复,Set集合不能存入重复的元素
	d1.a = 5; //这里改变了Demo类中参与hashCode运算变量a的值
	al.remove(d1); //这里删除d1对象的时候,找的是刚开始存入时候的hashCode,但因上面的语句改变了hashCode,所以他就找不到d1对象的位置,就删除不掉了。
	System.out.println(al.size()); //4 所以这里还是4个元素,若d1.a=5被注释,就是3个了。
	}
}
class Demo {
	public int a;
	Demo(int a) {
		this.a = a;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + a;
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Demo other = (Demo) obj;
		if (a != other.a)
			return false;
		return true;
	}


}

反射的作用 --> 实现框架功能
框架与框架要解决的核心问题:
我做房子卖给用户住,由用户自己安装门窗和空调,我做的房子就是框架,用户需要使用我的框架,把门窗插入进我提供的框架中。框架与工具类有区别,工具类被用户的类调用,而框架则是调用用户提供的类
框架要解决的核心问题:
我在写框架(房子)时,你这个用户可能还在上小学,还不会写程序,我写的框架程序怎样调用你以后写的类(门窗)呢?

因为在写程序时,无法知道要被调用的类名,所以,在程序中无法直接new某个类的实例对象,而要用放射方式来做。

---------------------- ASP.Net+Android+IOS开发.Net培训、期待与您交流! ----------------------详细请查看:http://edu.csdn.net

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值