反射


前些日子一直各种事情,不过在零散的时间片段里,我用word分段的记下了学习笔记,今天一起完成了。

反射的基石----->Class

一个类被加载到内存中的字节码,占用一片存储空间,这个空间里面的内容就是类的字节码,不同的类的字节码是不同的,所以他们在内存中的字节码是不同的,这一个个的空间可分别用一个个的对象来表示,这些对象显然具有相同的类型,这个类型是什么呢?这个类的名字就是Class

如何得到各个字节码对应的实例对象(Class类型)

  • 类名.class,例如,System.class
  • 对象.getClass() ,例如,new Date().getClass()
  • Class.forName("类名"),例如,Class.forName("java.util.Date")

一个面试题:Class.forName的作用是返回字节码,返回字节码的方式有两种,第一种是说这份字节码曾经被加载过,这份字节码已经待在java虚拟机中了,直接返回,还有一种是java虚拟机中没有这份字节码,用类加载器去加载,把加载进来的字节码缓存在JVM,以后直接返回。

九个预定义的Class实例对象

八个基本类型加一个void

  • 参看Class.siPrimitive方法的帮助
  • int.class==Integer.TYPE

数组类型的Class实例对象

Class.isArray()

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

测试代码如下:

public class ReflectTest {
	public static void main(String[] args) throws Exception {
		// TODO Auto-generated method stub
		String str1 = "abc";
		Class cls1 = str1.getClass();
		Class cls2 = String.class;
		Class cls3 = Class.forName("java.lang.String");
		System.out.println(cls1 == cls2);//true
		System.out.println(cls1 == 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.isPrimitive());//数组这种类型是不是原始类型呢?结果为false
		System.out.println(int[].class.isArray());//判断Class是不是数组	true	
        }
}

反射

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

一个类的每个成员都可以用相应的反射API类的一个实例对象来表示,通过调用Class类的方法可以得到这些实例对象。

Constructor类

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

  例如:Constructor [  ]  constructors=

          Class.forName("java.langlString").getConstructors();

得到某一个构造方法:

        Constructor constructor = 

           Class.forName("java.lang.String").getConstructor(StringBuffer.class);

通常情况下我们会这样做:

new String(new StringBuffer("abc"));

现在我们可以这样做:

   	Constructor constructor1 = String.class.getConstructor(StringBuffer.class);//可变参数列表,返回特定的构造函数
		String str2 = 
      (String)constructor1.newInstance(/*"abc"*/new StringBuffer("abc"));
	     /*
		 * 如果你填入abc,运行时会出现argument type mismatch异常,必须填你先前指定的构造方法,调用获得的方法时要用到上面相同类型的实例对象
		 * */


Class.newInstance() 提供一个便利的方法

  •   例如:String obj = (String)Class.forName("java.lang.String").newInstance();
  •   该方法内部先得到默认的构造方法,然后用该构造方法创建实例对象。
  •   该方法内部的具体代码用到了缓存机子来保存默认构造方法的实例对象。

Filed类

ReflectPoint类很简单,座标类,private int x; public int y;

ReflectPoint pt1 = new ReflectPoint(3,5);
		Field fieldY = pt1.getClass().getField("y");
		//fieldY的值是多少?是5,错!fieldY不是对象身上的变量,而是类上,要用它去取某个对象上对应的值
		System.out.println(fieldY.get(pt1));
		Field fieldX = pt1.getClass().getDeclaredField("x");//getFiled得到的只是可见的,而getDeclaredFiled可以得到不可见的
		fieldX.setAccessible(true);//设置可以访问,(暴力反射!)
		//这里有两个步骤,一个是说看的见,一个是说看的见拿不拿的到!
		System.out.println(fieldX.get(pt1));	

现在来看一个应用:将任意一个对象中的所有String类型的成员变量所对应的字符串内容中的“b”变成“a”。

我们还是在ReflectPoint这个类中做,增加三个字段如下:

    public String str1 = "ball";----->"aall"
	public String str2 = "basketball";------->aasketaall
	public String str3 = "itcast";  --------->itcast
    @Override
	public String toString(){
		return str1 + ":" + str2 + ":" + str3;
	}

方法如下:

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 oldValue = (String)field.get(obj);
							String newValue = oldValue.replace('b', 'a');
							field.set(obj, newValue);
						}
				}
	}


Method类

Method类代表某个类中的一个方法

得到类中的一个方法:

   例子:  Method charAt = 

           Class.forName("java.lang.String").getMethod("charAt",int.class);

调用方法:

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

   反射方式:System.out.println(charAt.invoke(str,1));

      如果传递给Method对象的invoke()方法的第一个参数为null,说明该method对象对应的是一个静态方法。

我们来看下面的代码:

String str1 = "abc";
Method methodCharAt = String.class.getMethod("charAt", int.class);
		//方法的名字和方法传入的参数列表
		System.out.println(methodCharAt.invoke(str1, 1));
		//第一个参数调用此方法的对象,第二个是传入的参数
		System.out.println(methodCharAt.invoke(str1, new Object[]{2}));

Jdk1.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的方法代码也可以这样写:

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

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

目标:

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

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

在程序中用静态方法调用我们是这样写的:

TestArguments.main(new String[]{"111","222","333"});

现在我们来用反射的方式来调用。为什么我们要用反射来调用呢?

String startingClassName = args[0];
		Method mainMethod = Class.forName(startingClassName).getMethod("main", String[].class);
		//mainMethod.invoke(null, new Object[]{new String[]{"111","222","333"}});
		//这样执行会导致异常:wrong number of arguments
		/*
		 * 启动java程序的main方法的参数是一个字符串数组,即public static void main(String[] args),通过
		 * 反射方式来调用这个main方法时,如何为invoke方法传递参数呢?按jdk1.5的语法,整数数组是一个
		 * 参数,而按jdk 1.4的语法,数组中的每一个元素对应一个参数,当把一个字符串数组作为参数传递给
		 * invoke方法时,javac会到底按照哪种语法进行处理呢?jdk1.5为了兼容jdk1.4的语法,会按jdk1.4的
		 * 语法进行处理,即把数组打散成若干个单独的参数。所以,在给main方法传递参数时,不能使用代码
		 * mainMethod.invoke(null,new String[]{"xxxx"}),javac只把它当作jdk1.4的语法进行理解,而不把
		 * 它当作jdk1.5的语法解释,因此会出现参数类型不对的问题。
		 * 解决的办法如下:
		 */
		mainMethod.invoke(null, new Object[]{new String[]{"111","222","333"}});
		mainMethod.invoke(null, (Object)new String[]{"111","222","333"});
		//编译时不把参数当作数组看待,也就不会把数组打散成若干个参数了

数组的反射

具有相同的维数和元素类型的数组属于同一个类型,即具有相同的Class实例对象。

int [] a1 = new int[]{1,2,3};
		int [] a2 = new int[4];
		int[][] a3 = new int[2][3];
		String [] a4 = new String[]{"a","b","c"};
		System.out.println(a1.getClass() == a2.getClass());//true 
		//System.out.println(a1.getClass() == a4.getClass());//false
		//System.out.println(a1.getClass() == a3.getClass());//false
		System.out.println(a1.getClass().getName());// [I
		//如果此类对象表示一个数组类,则名字的内部形式为:表示该数组嵌套深度的一个或多个 '[' 字符加元素类型名。
		System.out.println(a1.getClass().getSuperclass().getName());//java.lang.Object
		System.out.println(a4.getClass().getSuperclass().getName());//java.lang.Object


代表数组的Class实例对象的getSuperClass()方法返回的父类为Object类对应的Class。

基本类型的一维数组可以被当作Object类型使用,不能当做Object[]类型使用;非基本类型的一维数组,既可以当做Object类型使用,又可以当做Object[]类型数组使用。

Arrays.asList()方法处理int[] 和String[]时的差异。

        Object aObj1 = a1;
		Object aObj2 = a4;
		//Object[] aObj3 = a1;//基本类型的一维数组是不能转化为这种Object数组的
		int[] aObje3=a1;//这样就可以了
		Object[] aObj4 = a3;
		Object[] aObj5 = a4;
		
		System.out.println(a1);//[i@1cfb549
		System.out.println(a4);//[Ljava.lang.String;@186d4c1
		System.out.println(Arrays.asList(a1));//[[IA1cfb549
		System.out.println(Arrays.asList(a4));//[a,b,c]

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

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某个类的实例对象了,而要用反射方式来做。

综合案例

 先直接用new语句创建ArrayList和HashSet的实例对象,演示用eclipse自动生成ReflectPoint类的equals和hashCode方法,比较两个集合的运行结果差异。

       //哈希算法来提高集合中查找元素的效率,这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,
		//可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储在哪个区域。
		//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);	
		
		/*pt1.y = 7;		
		当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后
		的哈希值的字段了,否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即便
		在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导
		致无法从HashSet集合中单独删除当前对象,从而造成内出泄露。
		//collections.remove(pt1);
		内存泄露的典型案例*/
		
		System.out.println(collections.size());

改用配置文件加反射的方式创建ArrayList和HashSet的实例对象。

	/*getRealPath();//金山词霸/内部
		一定要记住用完整的路径,但完整的路径不是硬编码,而是运算出来的。*/
		//InputStream ips = new FileInputStream("config.properties");
		//考虑用类加载器顺带一起加载配置文件,但是这样只能读。
		//InputStream ips = ReflectTest2.class.getClassLoader().getResourceAsStream("cn/itcast/day1/config.properties");
		//在classpath指定的目录下逐一的去查找你要加载的文件
		//InputStream ips = ReflectTest2.class.getResourceAsStream("resources/config.properties");相对于我的包而言
		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 collections = (Collection)Class.forName(className).newInstance();


最后再写点类加载器的知识

类加载器

Java虚拟机中有多个类加载器,系统默认三个主要的类加载器,每个类负责加载特定位置的类:

BootStrapExtClassLoaderAppClassLoader

类加载器也是java类,因为其他是java类的类加载器本身也要被类加载器加载,显然必须有一个类加载器不是java类,这正是BootStrap

Java虚拟机中的所有类加载器采用具有父子关系的树形结构。

BootStrap-------->JRE/lib/rt.jar

ExClassLoader----->JRE/lib/ext/*.jar

AppClassLoader---->ClassPath指定的所有jar或目录

类加载器的委托机制

java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?

首先当前线程的类加载器去加载线程中的第一个类

如果类A中引用了类Bjava虚拟机将使用加载类A的类装载器来加载类B

还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类

每个类加载器加载类时,又先委托给其上级类加载器。

当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则抛出ClassNotFoundException,不再去找发起者类加载器的儿子,因为没有getChild方法,即便有,那有多个儿子,找哪一个呢?

思考:能不能自己写个类叫java.lang.System?(不能)

编写自己的类加载器

知识讲解

自定义的类加载器必须继承ClassLoader

loadClass方法与findClass方法

defineClass方法


至此终于写完了,不过写完后我感觉踏实了许多,因为每当我看自己写的日记时都能马上回忆起许多东西。


发布了25 篇原创文章 · 获赞 21 · 访问量 11万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章