黑马程序员——Java高新知识——反射

------Java培训、Android培训、iOS培训、.Net培训、期待与您交流!

一、概述

         在以后的程序开发中,我们会经常用到框架来进行开发,而框架的功能的实现就是通过反射。通过反射,可以实现框架对类的调用,提高了程序的扩展性。

二、Class类

       Class类是反射中的基础,学习反射首先要学习Class。

       Java中的类用于描述一类事物的共性,该类事物有什么属性,没有什么属性。至于属性的值是什么,则是由这个类的实例对象来确定,不同的实例对象有不同的属性值。Java程序中各个Java类,它们也属于同一类事物,而这一类事物就是Class。Class中描述了Java类的共性的信息,如类的名字、类的访问属性、类所在的包名、成员方法名称、继承的父类等等。说简单点,Java类是对具体事物的抽象,而Class类就是对Java类的抽象。

      Class类代表所有Java类,它的实例对象对应各个类在内存中的字节码,如String类的字节码、HashSet类的字节码。当一个类被加载进内存中,会占用一篇存储空间。这个空间里的存储内容就是类的字节码,不同的类的字节码是不同的。这一个个的空间可分别用一个一个的对象来表示,这些对象就是Class类的实例对象,它们所属的类型就是Class。可以理解为Class对应的实例对象就是一个Java类加载进内存后的存在形式。

      Class没有公共的构造方法,给Class类型变量赋值,得到Class类的实例对象有下面三种方法:

       (1) Class cls=类名.class;在这一方法中,出现了特有的类名,将该类加载进内存。生成一个类型是Class的字节码对象。

       (2) Class cls=对象名.getClass();出现对象,说明该类已加载进内存,通过对象名来获得字节码对象。也可以通过匿名对象来使用该方法。

       (3) Class  cls=Class.forName(完整的类名);例如:Class.forName("java.lang.String")。在调用该方法前,对应的Java类已加载进虚拟机,调用时直接获得字节码对象;另一种情况是该类之前没有加载到虚拟机,这时通过该方法,类加载器将该类加载进虚拟机。返回加载进来的字节码。这种方法的使用比较常见,因为编写源程序时,可能还不知道要调用的类的名字,可以用一个变量来表示。运行时从配置文件中加载。Class类中

         Java程序中有九个预定义的Class类对象,包括八种基本数据类型的字节码对象和void类型的字节码对象。在Java的源程序中,只要是类型,就有对应的Class实例对象。

         通过查阅API文档,我们可以看见Class类中定义了很多方法,下面就通过一段代码展示Class类的实例对象的获得与常用的方法,如下:

 public static void main(String[] args) throws ClassNotFoundException {
           
       //通过String类进行演示
       String str="你好";
       //通过类名建立
       Class cls1=str.getClass();
       //通过对象名建立
       Class cls2=String.class;
       //Class的静态调用
       Class cls3=Class.forName("java.lang.String");
      System.out.println(cls1==cls2);
      System.out.println(cls2==cls3);
      //获取对应的类名
      String name=cls1.getName();
      System.out.println(name);
      //判断是否是基本类型
      cls1.isPrimitive();
      //获取所在的包名
      cls1.getPackage();
	}

          注意的是通过三种方法获得的字节码对象是同一个,是因为虚拟机中相同的字节码只存在一份,并一直存在。下面再通过一段程序了解基本数据类型变量以及基本数据类型数组的字节码对象,如下:

public static void main(String[] args) throws ClassNotFoundException {
      
      //判断是否是基本数据类型
      System.out.println(int.class.isPrimitive());
      System.out.println(int[].class.isPrimitive());
	  //判断是否是数组类型
      System.out.println(int[].class.isArray());
      //基本数据类型与包装类进行比较
      System.out.println(int.class==Integer.class);
      System.out.println(int.class==Integer.TYPE);
  }

       注意int[].class不是基本类型,是数组类型的Class实例对象。基本数据类型的包装类.TYPE对应的是包装的基本类型的字节码。

三、反射

       就是把Java类中的各种成分映射成相应的Java类。就是一个Java类能用一个Class类的对象来表示,那么它的组成部分如成员变量、成员方法、构造函数、包等都可以用一个个Java类来表示。如同,人是一个类,人身上的心脏、人的眼睛也是一个类。Class类提供了方法,来获取其中的成员变量、成员方法、构造方法、修饰符、包等信息,这些信息就是用对应类的实例对象来表示,比如:Field、Method、Constructor、Package等等。

       一个类中的每一个成员可以用相对应的反射API类的一个实例对象来表示,通过调用Class的方法得到实例对象后的操作是学习反射知识的重点。下面就对这些API对象进行一一学习。

    (1)Constructor

       Constructor类代表某个类中的构造函数。可以通过获得Constructor类的对象,利用其方法,创建所在类的实例对象。

     通过Class类的getConstructor方法获取Constuctor的对象,要注意的是一个类中可能有无参数的构造方法,也可能有有参数的构造方法,想要获得有参数的构造方法,则需要将该参数类型的字节码传入。通过传入的参数类型,让Java程序选择获取指定的构造函数。

       通过Constructor中的newInstance方法,来创建对应类的实例对象,要注意的是如果有参数的构造方法,要传入对应的参数对象。下面通过一个程序进行整个过程:

 public static void main(String[] args) throws Exception{
      
	  //获取String类的所有构造函数
	  Constructor []constructors=String.class.getConstructors();
	  //获取带有StringBuilder参数的构造函数
	  Constructor con=String.class.getConstructor(StringBuilder.class);
	  //通过构造函数创建实例对象
	  String str=(String)con.newInstance(new StringBuilder("你好"));
	}
要注意几点:

      1.获取有参数的构造函数时,要传入参数类型的字节码。调用时要传入同样的类型对象。如果没有这么做,编译时不会报错,但运行时会出现错误。这时因为Java在编译时只看变量定义,只看语法上是否存在错误,不看代码执行,并不知道该构造方法是String类上的哪个构造函数。而运行时才知道对应哪个构造方法。在以后的程序中,要注意分清错误是在编译阶段还是运行阶段,进行对应的处理。

      2.如果构造函数没有设定泛型,建立的实例对象是Object类型的,需要强制转换为String类型。

      3.Class类可以直接通过newInstance方法,直接创建对应类的实例对象。例如:String str=(String)String.class.newInstance()

这时因为该方法内部先得到默认的构造方法,然后用该构造方法创建实例对象。通过查看该函数的内部代码,发现它应用了缓存机制来保存构造方法的实例对象。此构造方法是无参数的。

     (2) Field

      Field类代表某个类中的一个成员变量。可以通过该类的特有方法获取或修改对应类的对象的成员变量。   

      通过Class的getField方法获得的Field对象表示类上的成员变量的定义,而不是具体的变量。如果要获取的成员变量是非公有的,这时需要通过暴力反射来实现。下面通过一段代码进行演示:

public class ReflectDemo1 {

  public static void main(String[] args) throws Exception{
      
	   //建立Demo对象
	  	Demo d=new Demo(1,2,3);
	  //获取Demo类的所有非私有成员变量
	  	//Field[]fields=Class.forName("com.reflect.Demo").getFields();
	  //获取表示为x的成员变量,需要传入变量名
	     Field  fieldX=d.getClass().getField("x");
	  //调用对象,获得其上x变量的值。
	     int num=(Integer)fieldX.get(d);
	     System.out.println(num);
	   //获取表示为z的私有成员变量
	     Field fieldZ=Class.forName("com.reflect.Demo") .getDeclaredField("z");
	    //调用对象,设置其上z变量的值,需要用到暴力反射。
	     fieldZ.setAccessible(true);
	     fieldZ.set(d, 8);
	     System.out.println(d);
  }
 }
//定义一个类
class Demo{
	public int x;
    int y;
    private int z;
    Demo(int x, int y,int z) {
		this.x = x;
		this.y = y;
		this.z = z;
	}
	public String toString() {
		return "Demo [x=" + x + ", y=" + y + ", z=" + z + "]";
	}
	
}
要注意的是在上面的程序中,非公有的成员变量需要通过暴力反射的方式才能实现对象的变量值获取或修改 。     

      下面是一个利用反射修改对象变量的小练习,如下:

/**
需求:将任意一个对象张所有String类型的成员变量所对应的字符内容中的'b'改成‘a’;

*/
public class ReflectDemo {

	public static void main(String[] args) throws IllegalArgumentException, Exception {
		
          //建立Reflect类对象
		  Student stu=new Student("baibing","basic123",23);
		
		//通过反射,获得变量列表
		Field[]fields =Student.class.getDeclaredFields();
		 //遍历数组,并判断变量类型是否是字符串,符合,则通过反射改变对象的变量的值
		 for(Field field:fields){
			 if(field.getType()==String.class){
				 field.setAccessible(true);
				 String oldValue=(String)field.get(stu);
				 String newValue=oldValue.replace('b', 'a');
				 field.set(stu, newValue);
		    }
		 }
		 System.out.println(stu);
	}

}
//定义一个学生类
class Student{
	//定义属性
	private String name;
	private String id;
	private int age;
	public Student(String name, String id, int age) {
		this.name = name;
		this.id = id;
		this.age = age;
	}
	public String toString() {
		return "Reflect [age=" + age + ", id=" + id + ", name=" + name + "]";
	}
	
}
在spring框架中,就会用到这种方法对配置文件进行内容的扫描和替换。以后会学习到。

  (3)Method类

      Method代表某个类中的一个成员方法。可以通过该类的特有方法来调用对应类的对象,来实现方法的应用。

      通过getMethod方法获取Method时,要传入方法名和参数类型的字节码;而在用方法调用对象时则要传入对象名和参数。注意当调用时第一个参数为null时,说明对应静态方法。通过一段代码进行演示:

 

<pre name="code" class="java">public static void main(String[] args) throws Exception {
		
        //获得String类中charAt、valueOf方法。
		Method methodCharAt=String.class.getMethod("charAt", int.class);
		Method methodValueOf=String.class.getMethod("copyValueOf", char[].class);
		//利用invoke方法调用对象,实现功能。
		Character ch=(Character)methodCharAt.invoke("你好", 1);
		String str=(String)methodValueOf.invoke(null,new char[]{'你','好'});
		System.out.println(ch);
		System.out.println(str);
	}



注意:invoke方法在JDK1.5中的参数列表是(Object obj,Object...args),第二个参数接收的是一个可变数组,如果传入一个数组,会把整个数组当成一个参数;而在JDK1.4中对应的参数列表中第二个参数是Object[]args,是一个数组,在传入一个引用类型数组时它会把这个数组中每一个元素作为一个参数传入。由于JDK1.5要兼容1.4版本,当我们传入一个引用类型数组时,该方法就会依据JDK1.4的语法把数组中每一个元素作为一个参数;而传入一个基本类型数组时,则会将整个数组作为一个参数。下面通过一段调用一个类的主函数的程序演示,如下:

public class ReflectDemo {

	public static void main(String[] args) throws Exception {
		
        //获得Demo类中的主函数。
		Method methodMain=Demo.class.getMethod("main", String[].class);
	   //利用invoke方法实现功能。
	       methodMain.invoke(null, new Object[]{new String[]{"你好","再见"}});	        
	       methodMain.invoke(null,(Object)new String[]{"你好","再见"});	        
	}

}
class Demo{
	public static void main(String[]args){
		System.out.println(args[1]);
	}
}

       在上面的程序中,要注意的是如果在invoke方法中直接传入一个字符串数组参数,会造成参数长度不对的异常。这是因为JDK1.5要兼容JDK1.4,会按照1.4的语法的方式进行处理。JDK1.4中会把传入的字符串数组中每个元素作为一个参数,而该方法接收的参数是一个字符串数组,所以造成错误。这时我们就需要对传入的参数进行一定的修饰,可以把这个字符串数组放在一个Object类型数组中,作为它的一个元素,把这个Object类型数组传入,这样JDK1.4就会将打开这个Object数组后,把字符串数组作为一个参数;也可以将这个字符串数组用Object表示,运行时提醒Java程序这是一个Object类型的元素,这样也可成功调用方法。

        我们为什么要用反射方式来实现调用呢,这是因为编写程序时,有时我们并不知道类名,这时可以用主函数接收的传入参数来表示,然后在运行时传入指定的类名,实现方法的调用。如下: 

public static void main(String[] args) throws Exception {
		//用主函数的参数表示要传入的类名
        String className=args[0];
		//获得Demo类中的主函数。
		Method methodMain=Class.forName(className).getMethod("main", String[].class);
	   //利用invoke方法实现功能。
	       methodMain.invoke(null, new String[]{"你好","再见"});	        
	              
	}
这样做可以提高程序的扩展性。Java中这种获得方法对象,然后用方法对象使用自身方法的模式叫做专家模式。就是谁具有一项功能,就让它去实现这种功能。

     (4)数组的反射

          具有相同维数和元素类型的数组属于同一个类型,就是有相同的Class实例对象。通过代码进行展示:

 public static void main(String[] args) throws Exception{
        //定义int类型的一维数组
	    int[]a1=new int[3];
	    int[]a2=new int[4];
	    //定义int类型的二维数组
	    int[][]a3=new int[2][3];
	    //定义String类型的一维数组
	    String[]a4=new String[2];
	    //获取字节码对象,并进行比较
	    System.out.println(a1.getClass()==a2.getClass());
	    //System.out.println(a1.getClass()==a3.getClass());
	    //System.out.println(a1.getClass()==a4.getClass());
	    //利用Class对象的特有方法,获得父类
	        String str1=a1.getClass().getSuperclass().getName();
	        String str3=a3.getClass().getSuperclass().getName();
	        String str4=a4.getClass().getSuperclass().getName();
	        System.out.println(str1);
	        System.out.println(str3);
	        System.out.println(str4);
 }
       通过这段代码的运行结果我们可以知道:同样长度和元素类型相同的数组对应的是一个字节码对象。数组的实例对象通过getSuperClass()方法返回的父类对象为Object类的实例对象,表明数组的父类就是Object。注意的是,基本数据类型的一位数组可以当做Object类型使用,不能当做Object[ ]类来使用;引用数据类型的数组,既可以当Object类使用个,也可当做Object[ ]类型使用。

       在以前学习过的Arrays工具类的asList()方法中,如果传入的是一个引用数据类型数组,就会把它的元素作为集合中的每一个元素,而传入基本数据类型的时,它会把整个数组作为集合中的元素。这就是因为在JDK1.4的语法中,会把传入的Object[ ]中的每一个元素作为一个参数,而传入一个基本数据类型的数组时,则会把它看成一个Object类型的参数。

         在API的文档中,我们可以找到Array类,它用于完成对数组的反射操作,比如获取长度、获取、修改指定位置的元素等。通过下面的一段程序进行演示:

/**
需求:定义一个方法打印传入的对象,如果传入的对象是一个数组,则打印数组中的每一个元素。
*/
public class ReflectDemo {

	public static void main(String[] args) throws Exception {
		//调用方法
		printObj(123);
		printObj(new String[]{"你好","再见"});
		
	}
    //定义打印方法
	public static void printObj(Object obj){
		//判断对象是否是数组类型
		if(obj.getClass().isArray()){
			//通过Array的方法,获取数组长度
			int len=Array.getLength(obj);
			//遍历数组,获得并打印元素
			for(int x=0;x<len;x++){
				Object i=Array.get(obj, x);
				System.out.println(i);
			}
		}
			else
				System.out.println(obj);
		}
	}
(5)HashCode

         首先先看一段代码,对运行结果进行比较,如下:

public static void main(String[] args) {
		//定义一个ArrayList集合,存放字符串元素
		Collection<String> a1=new ArrayList<String>();
		a1.add("abc");
		a1.add("def");
		a1.add("gh");
		a1.add("abc");
		System.out.println(a1);
		System.out.println(a1.size());
		//定义一个HashSet集合,存放字符串元素
		Collection<String> a2=new HashSet<String>();
		a2.add("abc");
		a2.add("def");
		a2.add("gh");
		a2.add("abc");
		System.out.println(a2);
		System.out.println(a2.size());
	}
      从运行结果可以看出,在ArrayList集合中可以放入重复元素,而HashSet中不可以。这时因为ArrayList是把对象的引用有序的放入,而不对元素是否重复进行判断,并按照放入的顺序排序。而HashSet中不放重复元素是因为它会先判断元素hashCode(哈希值),如果出现hashCode相等,会再通过元素的equals方法,判断是否为同一个对象,相同则不会存入。

      hashCode方法:在HashSet集合中,把集合粉尘若干个区域,每个对象都算出一hashCode值,放在对应的区域,在这个区域中判断是否有相同的对象。该方法只适用在实现哈希算法的集合中。如下图:


      在HashSet存入自定义对象时,我们通常要复写对象的hashCode和equals方法,来判断对象是否相同,要注意的是当这个对象进如HashSet集合后,就不能修改这个对象中参与计算哈希值的字段的值了。这是由于如果进行了修改,生成了新的哈希值与存入集合时的哈希值不同。集合执行判断、删除动作时,会依据存入时的hashCode去寻找该对象,发现无法在集合中的指定区域找到该对象,会导致无法删除该对象,这样可能会造成内存泄露。如下:

public static void main(String[] args) {
		//定义一个HashSet集合,存放Person对象
		Collection<Person> a2=new HashSet<Person>();
		Person p1=new Person("xiaoming",23);
		Person p2=new Person("zhangsan",18);
		Person p3=new Person("aliang",19);
		Person p4=new Person("chenjie",16);
		a2.add(p1);
		a2.add(p2);
		a2.add(p3);
		a2.add(p4);
		//修改对象中计算hashCode的参数
		p2.age=13;
		//删除p2,会发现删除失败
		boolean b=a2.remove(p2);
		System.out.println(b);
		
	}

}
class Person{
	String name;
	int age;
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}
	//复写hashCode方法
	public int hashCode() {
		
		return name.hashCode()+age*27;
	}
	//复写equals方法,判断是否为相同对象
	public boolean equals(Object obj) {
		if(!(obj instanceof Person))
			throw new RuntimeException("类型不匹配");
		Person s=(Person)obj;
		return this.name.equals(s.name)&&this.age==s.age;
		
	}
	
}

四、反射的作用

        反射的作用就是实现框架功能。框架就是通过反射调用Java类,实现功能的方法。如同我们买一间毛坯房,房子就如同一个框架,我们在房子里安装门、窗户等等,就是在框架里调用用户提供的类;而我们在门上加锁,在窗上加入防盗栏,则是我们在门和窗再加入这些工具,就是用户类在调用工具类。框架类和工具类的区别就是:框架是调用用户提供的类,而工具类是被用户类调用。

         当一个房子建立时,并不知道以后被什么人住,安装什么。但会提供一个结构,住户到时候就可以直接将门窗等安装到贩子上。我们在程序时,可能无法知道以后要被调用的类名,所以在程序中无法直接创建某个类的实例对象,而要用反射来做。通过利用框架,实现反射,会提高程序的扩展性,效率高。所以在实际开发中很常见。

          实现框架程序反射的步骤:

           1,需要设置一个配置文件,一般文件名后缀为.properties,然后写入配置信息,以键值对的形式。左边是配置键,右边是配置的值。

           2.通过流对象,将文件内容加载进Properties集合中。

           3.通过getProperties方法获取配置的值,即指定的类名,用反射的方式创建该类对象。

              4.应用对象的功能

            下面通过一段建立集合的程序进行演示:

/**
 通过框架的反射方式建立一个集合,在配置文件中配置该集合的具体类型。
 */
public class Demo3 {
	
	public static void main(String[] args)throws Exception{
		//建立输入流对象和配置文件相关联
		InputStream is=new FileInputStream("config.properties");
		//建立Properties对象,并加载流
		Properties prop=new Properties();
		prop.load(is);
		is.close();
		//获取配置的类名
		String className=prop.getProperty("className");
		//通过获取的类名,建立对象
		Collection coll= (Collection)Class.forName(className).newInstance();
		//实现对象功能
		coll.add("abc");
		coll.add("cde");
		coll.add("fgh");
		
	}

}
加载配置文件也可以通过类加载器进行加载。效率更高。格式如:

       InputStream is =Demo3.class.getClassLoader().getResourceAsStream("config.properties");
或者

      InputStream is = Demo3.class.getResourceAsStream("config.properties");

注意: 传入配置文件时如果不是和ClassPath路径相关,则传入完整路径名;如果相关,可传入相对路径名。

        


 -------------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章