黑马程序员-java学习笔记-反射

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

反射就是把Java类中的各种成分映射成相应的java类。例如,一个Java类中用一个Class类的对象来表示,一个类中的组成部分:成员变量,方法,构造方法,包等等信息也用一个个的Java类来表示,就像汽车是一个类,汽车中的发动机,变速箱等等也是一个个的类。表示java类的Class类显然要提供一系列的方法,来获得其中的变量,方法,构造方法,修饰符,包等信息,这些信息就是用相应类的实例对象来表示,它们是Field、Method、Contructor、Package等等。 

反射学习要点

就是学会运用反射的API来获取一个类中各个成员对象,如成员变量、构造方法和普通方法,然后运用获取的对象进行操作。


1、Constructor类

通常我们编写一个类,会写一个构造函数,通过反射如何获取一个类的构造函数呢?Constructor类代表某个类中的一个构造方法

要获取一个类的构造函数,必须先获取该类的实例,这时就用到了Class类,再通过Class类的getConstructor()方法,我们可以这样获取一个Constructor对象

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

或者这样子

String.class.getConstructor(StringBuffer.class)
获取带两个参数的构造函数

String.class.getConstructor(StringBuffer.class,int.class)
为什么该方法可以接收一个、两个或者多个参数呢?

因为jdk1.5的新特性:可变参数

在jdk1.5之前是用一个数组来实现的

jdk1.4的文档声明如下:


通过getConstructor()方法得到了一个Contructor对象,就可以调用Contructor类的方法
Constructor constructor1 = String.class.getConstructor(StringBuffer.class);
String str2 = (String)constructor1.newInstance("abc");


结果报错了,为什么呢?

这里出现了两个StringBuffer
第一个表示你选择了哪个构造方法
第二个表示使用刚才选用的StringBuffer构造方法的时候,传递一个StringBuffer对象给该方法

编译时,只知道contructor1是一个构造类对象,它可以newInstance(),但是并不知道
调用了哪个(这里指String,String中有很多构造方法)构造方法,因此也并不知道返回了什么类型
所以这个会报错
String str2 = (String)constructor1.newInstance("abc");
运行时才知道到底调用了哪个构造方法  (通过后面的参数)
Constructor constructor1 = String.class.getConstructor(StringBuffer.class);
这个不会报错
String str2 = (String)constructor1.newInstance(new StringBuffer("abc"));

再通过一个例子进行强化

Constructor constructor1 = String.class.getConstructor(StringBuffer.class);
		String str2 = (String)constructor1.newInstance("abc"));
		System.out.println(str2.charAt(2));

上面代码可以正确编译
但是运行时会报错

因为contructor1对应的构造方法是StringBuffer("一个参数"),而在newInstance()中却传递了一个String类型
而编译器并不知道到底,只知道contructor1是一个构造方法,所以只有在运行的时候,才知道参数错误了,报错。

而Class类也提供了newInstance()方法,它调用的是不带参数的构造方法

Class.newInstance()方法:
例子:

String obj = (String)Class.forName("java.lang.String").newInstance();

该方法内部先得到默认的构造方法,然后用该构造方法创建实例对象。
该方法内部的具体代码是怎样写的呢?

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

总结:反射方式创建一个对象的过程
Class--Constructor----new object

可以看出反射是比较消耗性能的。


2、Field类

Field类代表某个类中的一个成员变量

使用技巧:eclipse生成构造方法的源代码


在介绍Field类之前,我们先编写一个测试类ReflectPoint

public class ReflectPoint {
	private int x;
	public int y;
	
	public ReflectPoint(int x, int y) {
		super();
		this.x = x;
		this.y = y;
	}
}

然后进行调用一下

ReflectPoint pt1 = new ReflectPoint(3,5);
Field fieldY = pt1.getClass().getField("y");

那fieldYd 值是多少呢?是5吗?

结果不是5!

因为fieldY不是对象身上的变量,而是类上,要用它去取某个对象上对应的值

所以 fieldY不代表具体的值,不是pt1身上的变量Y,而是代表ReflectPoint类上,要用它去取某个对象(不如pt1)
所对应的值
要取出fieldY对象所对应的值应该这样
System.out.println(fieldY.get(pt1));
既然知道了取值的方式, 用刚才的方法来取一下x的值试试

Field fieldX = pt1.getClass().getField("x");
System.out.println(fieldX.get(pt1));
结果报错了!


因为x是私有的,如果要去私有的成员变量应该用如下代码
Field fieldX = pt1.getClass().getDeclaredField("x");
System.out.println(fieldX.get(pt1));
然后高兴地点击运行,还是不行啊!报错了


这是需要暴力反射了,通过setAccesible(true)来设置私有成员可以访问
Field fieldX = pt1.getClass().getDeclaredField("x");
fieldX.setAccessible(true);
System.out.println(fieldX.get(pt1));

这里有个需求:将任意一个对象中的所有String类型的成员变量所对应的字符串内容中的"b"改成"a"。

我们对之前编写的ReflectPoint类进行扩展,覆盖该类的toString()方法

public class ReflectPoint {
	private Date birthday = new Date();
	
	private int x;
	public int y;
	public String str1 = "ball";
	public String str2 = "basketball";
	public String str3 = "itcast";
	
	public ReflectPoint(int x, int y) {
		super();
		this.x = x;
		this.y = y;
	
	@Override
       public String toString(){
       return str1 + ":" + str2 + ":" + str3;}
覆盖该类的toString()方法是方便观看结果


接着编写功能方法

private static void changeStringValue(Object obj) throws Exception {
		Field[] fields = obj.getClass().getFields();//获取目标对象的所有成员对象
		for(Field field : fields){//开始遍历
			//if(field.getType().equals(String.class)){
			if(field.getType() == String.class){//成员变量的类型是否是String
				String oldValue = (String)field.get(obj);
				String newValue = oldValue.replace('b', 'a');//a代替b
				field.set(obj, newValue);//将值设置进目标对象里面
			}
		}

因为字节码在内存只存在一份,所以比较字节码时用 == ,显得更加专业!
最后我们来测试一下
ReflectPoint pt1 = new ReflectPoint(3,5);
changeStringValue(pt1);
System.out.println(pt1);
这个方法小小的模拟了一下字段的反射,spring架构也是如此,修改配置文件中的字段。

3、Method类
Method类代表某个类中的一个成员方法

在平时的编码中
调用String类的charAt()方法,是通过构造出该类的对象,通过对象去调用该方法,如
str1.chatAt();
str2.charAt();
从中可以看出,对象与方法并没有绑定,方法是属于类的
利用反射的方式,先获取某个字节码的方法,再用获得的方法去作用某个对象

Method methodCharAt = String.class.getMethod("charAt", int.class);
		System.out.println(methodCharAt.invoke(str1, 1));

getMethod() //第一个参数传递的是方法名字,第二个参数是方法中的参数,这个是可变的
invoke() //第一个参数是指需要调动的对象,第二个是charAt()的参数

这里再次体现了面向对象的思想
methodCharAt表示一个方法对象,invoke是Method类的一个方法,表示调用
就像人在黑板画圆一样,画圆的方法应该由圆提供的,这里的调用方法也是有方法类提供的
静态方法的调用
静态方法不需要传递对象,所以传递一个Null
System.out.println(methodCharAt.invoke(null, 1));
dk1.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(“str”, new Object[]{1})形式。

1.4的调用形式

System.out.println(methodCharAt.invoke(str1, new Object[]{2}));


4、用反射执行某个类的main()方法

按照以往的编码经验,可以使用调用静态方法一样,调用main()方法
TestArguments.main(new String[] {"111","222","333"});
按照上面的知识点我们用反射的方式继续努力调用mian()方法

String startingMethodClassName=args[0];
Method mainMethod=Class.forName(startingMethodClassName.getMethod("main"),String[].class);

args[0]在这里代表一个类的完整名字
注意数组类型的传递,      String[].class
获取方法后调用该方法
mainMethod.invoke(null,new String[]{"111","222","333"});

但是运行会报错, 因为遇到数组会进行拆包 

启动Java程序的main方法的参数是一个字符串数组,即public static void main(String[] args),通过反射方式来调用这个main方法时,如何为invoke方法传递参数呢?
按jdk1.5的语法,整个数组是一个参数,而按jdk1.4的语法,数组中的每个元素对应一个参数。

当把一个字符串数组作为参数传递给invoke方法时,javac会到底按照哪种语法进行处理呢?
jdk1.5肯定要兼容jdk1.4的语法,会按jdk1.4的语法进行处理,即把数组打散成为若干个单独的参数。
所以,在给main方法传递参数时,不能使用代码mainMethod.invoke(null,new String[]{“xxx”}),javac只把它当作jdk1.4的语法进行理解,而不把它当作jdk1.5的语法解释,因此会出现参数类型不对的问题。

解决方法     关键字:每一个数组都作为一个对象来看待
mainMethod.invoke(null,new Object[]{new String[]{"111","222","333"}});
解决方法二 利用欺骗手法,让编译器认为不是数组,不进行拆分
mainMethod.invoke(null,(Object)new String[]{"111","222","333"});


5、数组的反射

我们先来看看下面的代码

int [] a1 = new int[3];
		int [] a2 = new int[4];
		int[][] a3 = new int[2][3];
		String [] a4 = new String[3];
		System.out.println(a1.getClass() == a2.getClass());
		System.out.println(a1.getClass() == a4.getClass());
		System.out.println(a1.getClass() == a3.getClass());

如果维数一样,而且类型相同,得到的字节码是一样的
上面代码中,最下面两个语句报错,而第一条语句打印true。

打印这些数组的父类的名字
System.out.println(a1.getClass().getSuperclass().getName());
System.out.println(a4.getClass().getSuperclass().getName());

结果发现都是java.lang.Object

小总结

1)具有相同维数和元素类型的数组属于同一个类型,即具有相同的Class实例对象。
2)代表数组的Class实例对象的getSuperClass()方法返回的父类为Object类对应的Class。
3)基本类型的一维数组可以被当作Object类型使用,不能当作Object[]类型使用;非基本类型的一维数组,既可以当做Object类型使用,又可以当做Object[]类型使用。

分析一下下面的代码

                Object aObj1 = a1;
		Object aObj2 = a4;
		Object[] aObj3 = a1;
		Object[] aObj4 = a3;
		Object[] aObj5 = a4;

Object[] aObj4=a3;表示装了Object[]数组中装着int[] 的一维数组
但是由于a1是基本类型,所以Object[] aObj3=a1则行不通
a4表示String ,父类是Object

Array工具类用于完成对数组的反射操作


实现一个方法,打印Object,如果传递的是数组,打印数组的每个值
private static void printObject(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);//不是数组正常打印
		}
		
	}

反射实现框架

框架与框架要解决的核心问题

我做房子卖给用户住,由用户自己安装门窗和空调,我做的房子就是框架,用户需要使用我的框架,把门窗插入进我提供的框架中。框架与工具类有区别,工具类被用户的类调用,而框架则是调用用户提供的类。

框架要解决的核心问题
我在写框架(房子)时,你这个用户可能还在上小学,还不会写程序呢?我写的框架程序怎样能调用到你以后写的类(门窗)呢?
因为在写才程序时无法知道要被调用的类名,所以,在程序中无法直接new 某个类的实例对象了,而要用反射方式来做。

HashSet的比较以及Hashcode分析
我们继续利用上文提到的ReflectPoint进行分析
               Collection collections = new HashSet();
		ReflectPoint pt1 = new ReflectPoint(3,3);
		ReflectPoint pt2 = new ReflectPoint(5,5);
		ReflectPoint pt3 = new ReflectPoint(3,3);	

		collections.add(pt1);
		collections.add(pt2);
		collections.add(pt3);
		collections.add(pt1);	
		System.out.println(collections.size());

打印结果是3,最后一个没有存进去,那pt1和pt3是否是相等呢?
答案是:不相等。
如果要这个两个对象相等,必须自己去写equals()方法,默认的equals比较的是hashcode的值,该值通常用内存地址值换算出来的。
我们可以在eclipse中,右键生成hashcode()和equals()方法




最后得到代码

@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;
		final ReflectPoint other = (ReflectPoint) obj;
		if (x != other.x)
			return false;
		if (y != other.y)
			return false;
		return true;
	}

这样之后,打印结果就是2了。
hashcode()方法的作用
没有覆盖hashcode()方法,那结果会如何呢?
答案:可能是2,可能是3
面试题:java中有内存泄露吗?为什么?举例说明。
可以上面引用这个。
通过反射的方式创建ArrayList和HashSet对象
在config.properties文件中写入
配置文件到底放在哪里呢?      
实际开发中要用完整路径,但完整路径不是硬编码,而是运行的时候出来的


然后反射
这里尽量用继承的方式,如InputStream、Collection
InputStream ips = ReflectTest2.class.getResourceAsStream("/cn/itcast/day1/resources/config.properties");
		Properties props = new Properties();
		props.load(ips);//读取配置文件
		ips.close();
		String className = props.getProperty("className");
		//通过反射的方式得到Collection
		//这里的newInstance()调用的是无参数的构造方法
		Collection collections = (Collection)Class.forName(className).newInstance();
		ReflectPoint pt1 = new ReflectPoint(3,3);
		ReflectPoint pt2 = new ReflectPoint(5,5);
		ReflectPoint pt3 = new ReflectPoint(3,3);	

		collections.add(pt1);
		collections.add(pt2);
		collections.add(pt3);
		collections.add(pt1);			
		System.out.println(collections.size());
用类加载器管理资源和配置文件
配置文件放在classpath所指定的文件
InputStream ips=ReflectTest2.class.getClassLoader().getResourceAsStream("cn/itcast/day1/config.properties");

Class本身也提供了getRourceAsStream()
该方法只需要指定文件名,该方法会自动找该类所在包(cn/itcast/day1)
InputStream ips = ReflectTest2.class.getResourceAsStream("/cn/itcast/day1/resources/config.properties");
如果在该包再建立一个子包(cn/itcast/day1/resource),将配置文件放入其中,该怎么办呢?
则只需要修改成:("resource/config.properties")

这些方法内部都是调用了classLoader()方法

总结:
需把配置文件放到classpath中,
在elipse中开发的时候,建立一个resouce文件放入其中,然后eclipse会自动复制配置文件到
classpath中


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

详细请查看:http://edu.csdn.net

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值