談談Java反射機制
寫在前面: 什麼是java反射機制?我們又為什麼要學它? 當程式執行時,允許改變程式結構或變數型別,這種語言稱為動態語言。我們認為java並不是動態語言,但是它卻有一個非常突出的動態相關機制,俗稱:反射。 IT行業裡這麼說,沒有反射也就沒有框架,現有的框架都是以反射為基礎。在實際專案開發中,用的最多的是框架,填的最多的是類,反射這一概念就是將框架和類揉在一起的調和劑。所以,反射才是接觸專案開發的敲門磚!
一、Class類
什麼是Class類?
在面向物件的世界裡,萬事萬物皆是物件。而在java語言中,static修飾的東西不是物件,但是它屬於類。普通的資料型別不是物件,例如:int a = 5;
Class A{}
當我建立了A類,那麼類A本身就是一個物件,誰的物件?java.lang.Class的例項物件。
那麼這個物件又該怎麼表示呢?
我們先看一下下面這段程式碼:
public class Demo(){ F f=new F(); } class F{}
這裡的F的例項化物件就可以用f表達出來。同理F類也是一個例項化物件,Class類的例項化物件。我們可以理解為任何一個類都是Class類的例項化物件,這種例項化物件有三種表示方法:
public class Demo(){ F f=new F(); //第一種表達方式 Class c1=F.class;//這種表達方式同時也告訴了我們任何一個類都有一個隱含的靜態成員變數class //第二種表達方式 Class c2=f.getClass();//這種表達方式在已知了該類的物件的情況下通過getClass方法獲取 //第三種表達方式 Class c3 = null; try { c3 = Class.forName("com.text.F");//類的全稱 } catch (ClassNotFoundException e) { e.printStackTrace(); } } class F{}
以上三種表達方式,c1,c2,c3都表示了F類的類型別,也就是官方解釋的Class Type。 那麼問題來了:
System.out.println(c1 == c2)? or System.out.println(c1 == c3)?
答案是肯定的,返回值為ture。這表明不論c1 or c2 or c3都代表了F類的類型別,也就是說一個類只可能是Class類的一個例項物件。 理解了Class的概念,我們也可以通過類的類型別建立該類的物件例項,用c1 or c2 or c3的newInstance()方法:
Public class Demo1{ try { Foo foo = (Foo)c1.newInstance();//foo就表示F類的例項化物件 foo.print(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); }} class F{ void print(){ } }
這裡需要注意的是,c1是F類的類型別,創建出來的就是F類的物件。如果a是A類的類型別,那麼創建出來的物件也應該與之對應,屬於A類的物件。
二、方法的反射 Class類有一個最簡單的方法,getName():
public class Demo2 { public static void main(String[] args) { Class c1 = int.class;//int 的類型別 Class c2 = String.class;//String類的類型別 Class c3 = void.class; System.out.println(c1.getName()); System.out.println(c2.getName()); System.out.println(c2.getSimpleName()); System.out.println(c3.getName()); } }
本的資料型別以及void關鍵字都是存在類型別的。
案例:
public class ClassUtil { public static void printClassMethodMessage(Object obj){ //要獲取類的資訊》》首先我們要獲取類的類型別 Class c = obj.getClass(); //我們知道Object類是一切類的父類,所以我們傳遞的是哪個子類的物件,c就是該子類的類型別。 //接下來我們要獲取類的名稱 System.out.println("類的名稱是:"+c.getName()); /* *我們知道,萬事萬物都是物件,方法也是物件,是誰的物件呢? * 在java裡面,方法是Method類的物件 *一個成員方法就是一個Method的物件,那麼Method就封裝了對這個成員 *方法的操作 */ //如果我們要獲得所有的方法,可以用getMethods()方法,這個方法獲取的是所有的Public的函式,包括父類繼承而來的。如果我們要獲取所有該類自己宣告的方法,就可以用getDeclaredMethods()方法,這個方法是不問訪問許可權的。 Method[] ms = c.getMethods();//c.getDeclaredMethods() //接下來我們拿到這些方法之後幹什麼?我們就可以獲取這些方法的資訊,比如方法的名字。 //首先我們要迴圈遍歷這些方法 for(int i = 0; i < ms.length;i++){ //然後可以得到方法的返回值型別的類型別 Class returnType = ms[i].getReturnType(); //得到方法的返回值型別的名字 System.out.print(returnType.getName()+" "); //得到方法的名稱 System.out.print(ms[i].getName()+"("); //獲取引數型別--->得到的是引數列表的型別的類型別 Class[] paramTypes = ms[i].getParameterTypes(); for (Class class1 : paramTypes) { System.out.print(class1.getName()+","); } System.out.println(")"); } } }
總結思路: 通過方法的反射得到該類的名稱步驟: 1.獲取該類的類型別 2.通過類型別獲取類的方法(getMethods()) 3.迴圈遍歷所獲取到的方法 4.通過這些方法的getReturnType()得到返回值型別的類型別,又通過該類型別得到返回值型別的名字 5.getName()得到方法的名稱,getParameterTypes()獲取這個方法裡面的引數型別的類型別。
三、成員變數的反射 首先我們需要認識到成員變數也是物件,是java.lang.reflect.Field類的物件,那麼也就是說Field類封裝了關於成員變數的操作。既然它封裝了成員變數,我們又該如何獲取這些成員變數呢?它有這麼一個方法:
public class ClassUtil { public static void printFieldMessage(Object obj){ Class c = obj.getClass(); //Field[] fs = c.getFields(); }
這裡的getFields()方法獲取的所有的public的成員變數的資訊。和方法的反射那裡public的成員變數,也有一個獲取所有自己宣告的成員變數的資訊:
Field[] fs = c.getDeclaredFields();
我們得到它之後,可以進行遍歷(既然封裝了Field的資訊,那麼我們就可以得到Field型別)
for (Field field : fs) { //得到成員變數的型別的類型別 Class fieldType = field.getType(); String typeName = fieldType.getName(); //得到成員變數的名稱 String fieldName = field.getName(); System.out.println(typeName+" "+fieldName); }
四、建構函式的反射 不論是方法的反射、成員變數的反射、建構函式的反射,我們只需要知道:要想獲取類的資訊,首先得獲取類的類型別。
public static void printConMessage(Object obj){ Class c = obj.getClass(); /* * 首先建構函式也是物件,是java.lang.Constructor類的物件 * 也就是java.lang. Constructor中封裝了建構函式的資訊 * 和前面說到的一樣,它也有兩個方法: * getConstructors()方法獲取所有的public的建構函式 * getDeclaredConstructors()方法得到所有的自己宣告的建構函式 */ //Constructor[] cs = c.getConstructors(); Constructor[] cs = c.getDeclaredConstructors(); for (Constructor constructor : cs) { //我們知道構造方法是沒有返回值型別的,但是我們可以: System.out.print(constructor.getName()+"("); //獲取建構函式的引數列表》》得到的是引數列表的類型別 Class[] paramTypes = constructor.getParameterTypes(); for (Class class1 : paramTypes) { System.out.print(class1.getName()+","); } System.out.println(")"); } }
五、Class類的動態載入類 如何動態載入一個類呢? 首先我們需要區分什麼是動態載入?什麼是靜態載入?我們普遍認為編譯時刻載入的類是靜態載入類,執行時刻載入的類是動態載入類。我們舉一個例子:
Class A{ Public static void main(String[] args){ if("B".equal(args[0])){ B b=new B(); b.start(); } if("C".equal(args[0])){ C c=new C(); C.start(); } } }
上面這一段程式碼,當我們在用eclipse或者myeclipse的時候我們並不關心是否能夠通過編譯,當我們直接在cmd使用javac訪問A.java類的時候,就會丟擲問題:
A.java:7:錯誤:找不到符號 B b=new B(); 符號: 類B 位置: 類A A.java:7:錯誤:找不到符號 B b=new B(); 符號: 類B 位置: 類A A.java:12:錯誤:找不到符號 C c=new C(); 符號: 類C 位置: 類A A.java:12:錯誤:找不到符號 C c=new C(); 符號: 類C 位置: 類A 4個錯誤
或許我們理所當然的認為這樣應該是錯,類B根本就不存在。但是如果我們多思考一下,就會發現B一定用嗎?不一定。C一定用嗎?也不一定。那麼好,現在我們就讓B類存在
Class B{ Public static void start(){ System.out.print("B...satrt"); } }
現在我們就先 javac B.class,讓B類先開始編譯。然後在執行javac A.class。結果是:
A.java:12:錯誤:找不到符號 C c=new C(); 符號: 類C 位置: 類A A.java:12:錯誤:找不到符號 C c=new C(); 符號: 類C 位置: 類A 2個錯誤
我們再想,這個程式有什麼問題。如果你說沒有什麼問題?C類本來就不存在啊!那麼問題來了B類已經存在了,假設我現在就想用B,我們這個程式用得了嗎?答案是肯定的,用不了。那用不了的原因是什麼?因為我們這個程式是做的類的靜態載入,也就是說new建立物件是靜態載入類,在編譯時刻就需要載入所有的,可能使用到的類。所以不管你用不用這個類。 現在B類是存在的,但是我們這個程式仍然用不了,因為會一直報C類有問題,所以B類我也用不了。那麼在實際應用當中,我們肯定需要如果B類存在,B類我就能用,當用C類的時候,你再告訴我錯了。如果說將來你有100個類,只要其中一個類出現問題,其它99個類你都用不了。所以這並不是我們想要的。 我們想要的就是我用那個類就載入那個類,也就是常說的執行時刻載入,動態載入類。如何實現動態載入類呢?我們可以建這麼一個類:
Class All{ Public static void start(){ try{ Class cl= Class.forName(args[0]); //通過類型別,建立該類的物件 cl.newInstance(); }catch(Exception e){ e.printStackTrace(); } } }
前面我們在分析Class例項化物件的方式的時候,Class.forName(“類的全稱”),它不僅僅表示了類的類型別,還表示了動態載入類。當我們javac All.java的時候,它不會報任何錯誤,也就是說在編譯的時候是沒有錯誤的。只有當我們具體用某個類的時候,那個類不存在,它才會報錯。 如果載入的類是B類,就需要:
B bt = (B) cl.newInstance();
萬一載入的是C類呢,可以改成
C ct = (C) cl.newInstance();
但是如果我想用很多的類或者載入很多的類,該怎麼辦?我們可以統一一個標準,不論C類還是B類或者其他的類,比如定義一個標準
Stand s = (Stand) cl.newInstance();
只要B類和C類都是這個標準的就行了。
Class All{ Public static void start(){ try{ Class cl= Class.forName(args[0]); //通過類型別,建立該類的物件 Stand s = (Stand) cl.newInstance(); s.start(); }catch(Exception e){ e.printStackTrace(); } } } interface Stand { Public void start(); }
現在如果我想要用B類,我們只需要:
Class B implements Stand{ Public void start(){ System.out.print("B...satrt"); } }
載入B類,編譯執行。
javac B.java javac Stand.java java Stand B
結果:
B...satrt
如果以後想用某一個類,不需要重新編譯,只需要實現這個標準的介面即可。只需要動態的載入新的東西就行了。 這就是動態載入類。