反射


前些日子一直各種事情,不過在零散的時間片段裏,我用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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章