黑馬程序員——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培訓、期待與您交流! -------

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