Java反射初探 ——「當類也學會照鏡子」

反射的做用

開門見山地說說反射的做用
 
1.爲咱們提供了全面的分析類信息的能力
2.動態加載類
 
我理解的「反射」的意義
(僅我的理解哈)
 
我理解的java反射機制就是: 提供一套完善而強大的API「反射「類的結構。
打個比方,反射機制就像是一面鏡子,而類就像是一個在照着鏡子的人。
鏡子(反射機制)照出(反射)了人的全貌(類的全方位的信息,例如方法,成員變量和構造器等的相關信息)
爲何要照鏡子? 由於不照鏡子看不清楚本身的全貌,「鏡子」就是爲了解決這個問題出現的(爲咱們提供全面分析類的能力)

好吧,我知道這聽起來仍是很模糊,讓咱們一步一步來:java

類也是對象

在java裏有一句話:萬物皆對象, 即便是int等基本類型,雖然本質上不是對象,但行爲卻也和對象密切相關(基本包裝類型和自動裝箱)
 
因此有一個可能徹底打破咱們常規思惟的論斷是: 類也是對象

「類」對象和「類」類型

好吧,其實說「 類也是對象」並不太好,而應該說,java中每一個類都有一個與之對應的「類」對象(Class對象),這個「類」對象由jvm生成,並保存了對應類的相關信息。例如,假設咱們的java文件涉及三個類:a類,b類和c類,那麼編譯的時候就會對應生成a類的「類」對象,a類的「類」對象,a類的「類」對象,分別用於保存和a,b,c類對應的信息
 
咱們的思惟是這樣的: 一個對象必然有一個與之對應的類,由於只有類才能實例化對象啊
 
那麼,「類對象」的「上面」,應該還有一個類纔對!這個「類之上的類」,就是java.lang.Class,它是全部「類」對象的類(這樣說可能聽起來很拗口)
 
咱們這樣聲明一個「類」對象,假設這個類對象(Class對象)是a——
Class a
咱們稱a屬於「類」類型(Class類型)
 
因此咱們能夠其實能夠將java中的對象分爲兩種:
 
1. 實例對象
2. Class對象
 
因此咱們今天要講的第一個內容是:  有別於平時使用的實例對象的——Class對象

取得Class對象的三種方式

咱們假設有這麼一個類叫MyClass:
public class MyClass {  }
那麼取得該類對應Class對象的方法有三種:
 
一. 經過「類名.class」的方式取得
Class classInstance= MyClass.class;

二. 經過類建立的實例對象的getClass方法取得數組

MyClass myClass = new MyClass();
Class classInstance = myClass.getClass();

三.經過Class類的靜態方法forName方法取得(參數是帶包名的完整的類名)eclipse

Class classInstance = Class.forName("mypackage.MyClass");
【注意】
 
1.運行forName時候可能會由於找不到包名而拋出已檢查異常ClassNotFoundException,因此咱們須要將其包裹在try-catch語句中:
複製代碼
try {
  Class classInstance = Class.forName("mypackage.MyClass");
} catch (ClassNotFoundException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}
複製代碼
2.上面三種方法取得的對象都是相同的,因此效果上等價

利用反射API全面分析類的信息——方法,成員變量,構造器

咱們上面提到,反射的一大做用是用於分析類的結構,或者說用於分析和這個類有關的全部信息。而這些信息就是類的基本的組成: 方法,成員變量和構造器
 
事實上,和咱們上面所介紹的Class類和Class對象類似的是,一個類中的方法,成員變量和構造器也分別對應着一個對象
 
1.每一個方法都對應有一個保存和該方法有關信息的Method對象, 這個對象所屬的類是java.lang.reflect.Method;
2.每一個成員變量都對應有一個保存和該變量有關信息的Field對象,這個對象所屬的類是 java.lang.reflect.Field
3. 每一個構造器都對應有一個保存和該構造器有關信息的Constructor對象,這個對象所屬的類是java.lang.reflect.Constructor
 
方法,成員變量和構造器是附屬於某一個類的,正因如此,咱們應該先取得某一個類對應的Class對象,其次才考慮如何取得 Method/Field/Constructor對象
 
咱們能夠經過一系列的方法,從一個類的Class對象中取得對應的Method對象,Field對象和Constructor對象
 
假設c是一個類的Class對象:
 
經過 c.getDeclaredMethods()可取得這個類中全部聲明方法對應的Method對象組成的數組
經過 c.getDeclaredFields()可取得這個類中全部聲明的成員變量對應的Field對象組成的數組
經過 c.getConstructors(); 可取得這個類中全部構造函數所對應的Constructor對象所組成的數組
 
在下面的示例中,咱們將遍歷某一個類中方法,成員變量和構造器的名稱:
 
MyClass.java:
複製代碼
public class MyClass {
  private int value; //成員變量
  public MyClass (int value) { this.value = value;   } //構造函數
  public int getValue() {  return value; } //方法1
  public void setValue(int value) {  this.value = value;  } //方法2
}
複製代碼
Test.java:
複製代碼
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
 
public class Test {
  public static void printClassMessage (Object obj) {
    Class c = obj.getClass(); // 獲取obj所屬類的Class對象
Method [] methods = c.getDeclaredMethods(); // 獲取方法對象列表
System.out.println("遍歷MyClass類裏的全部方法的名稱:"); for(int i =0; i<methods.length; i++) { System.out.println(methods[i].getName()); }
Field [] fields = c.getDeclaredFields();   // 獲取成員變量對象列表 System.out.println("遍歷MyClass類裏的全部成員變量的名稱:"); for(int i =0; i<fields.length; i++) { System.out.println(fields[i].getName()); }   Constructor [] constructors = c.getConstructors();  // 獲取構造函數對象列表 System.out.println("遍歷MyClass類裏的全部構造函數的名稱:"); for(int i =0; i<constructors.length; i++) { System.out.println(constructors[i].getName()); } }    

public static void main(String [] args) {     MyClass myClass = new MyClass(1); // 建立一個MyClass對象     printClassMessage(myClass);  // 打印這個對象所屬類的相關信息   } }
複製代碼
運行結果:
複製代碼
遍歷MyClass類裏的全部方法的名稱:
getValue
setValue
遍歷MyClass類裏的全部成員變量的名稱:
value
遍歷MyClass類裏的全部構造函數的名稱:
mypackage.MyClass
複製代碼

上面的例子僅僅是做爲一個展現,Method/Field/Constructor對象的API固然不只限於getName這樣獲取名稱的簡單操做,因此接下來我將分別介紹更具體的反射APIjvm

利用反射API分析類中方法信息

getMethods和getDeclaredMethods方法

getMethods和getDeclaredMethods的區別在於:
 
getMethods取得的method對應的方法包括從父類中繼承的那一部分,而
getDeclaredMethods取得的method對應的方法不包括從父類中繼承的那一部分
 
public Method[] getMethods()返回某個類的全部公用(public)方法包括其繼承類的公用方法,固然也包括它所實現接口的方法。
public Method[] getDeclaredMethods()對象表示的類或接口聲明的全部方法,包括公共、保護、默認(包)訪問和私有方法,但不包括繼承的方法。固然也包括它所實現接口的方法。
 
例如上面經過打印getDeclaredMethods打印的MyClass的方法信息:
getValue
setValue
 讓咱們看看經過getMethods打印又會取得什麼結果:
 
Test.java:
複製代碼
import java.lang.reflect.Method;
 
public class Test {
  public static void printMethodsMessage (Object obj) {
    Class c = obj.getClass();
    Method [] methods = c.getMethods();
    for (Method method : methods) {
      System.out.println(method.getName());
    }
  }
    
public static void main(String [] args) {     MyClass myClass = new MyClass(1);     printMethodsMessage(myClass);   } }
複製代碼
運行結果:
複製代碼
getValue
setValue
wait
wait
wait
equals
toString
hashCode
getClass
notify
notifyAll
複製代碼
讓咱們思考一下: 一個方法有哪些「信息」值得(或須要)咱們去分析呢?
 
主要是兩部分:
1. 返回值
2. 方法參數
你可能猜的出來, Method對象已經提供了一套API去獲取這些信息了,讓咱們來看看:

經過method.getReturnType()獲取方法返回值對應的Class對象

複製代碼
import java.lang.reflect.Method;
 
public class Test {
  public static void printMethodsMessage (Object obj) {
    Class c = obj.getClass();  // 取得obj所屬類對應的Class對象
    Method [] methods = c.getDeclaredMethods(); // 取得obj所屬類中方法對應的Method對象組成的數組
    for (Method method : methods) { // 遍歷Method對象
      String name = method.getName();   // 取得方法名
      Class returnClass = method.getReturnType(); // 獲取方法返回值對應的Class對象
      String returnName = returnClass.getName();  //獲取返回值所屬類的類名——也即返回值類型
      System.out.println(name + "方法的返回值類型是" + returnName);
    }
  }
  public static void main(String [] args) {
    MyClass myClass = new MyClass(1);
    printMethodsMessage(myClass);
  }
}
複製代碼
運行結果:
getValue方法的返回值類型是int
setValue方法的返回值類型是void
經過method.getReturnType(),咱們取得了該方法的返回值對應的Class對象(哈哈,繞了一圈後仍是回到Class對象上了)
而後經過Class對象調用getName方法就取得了返回值所屬的類的名稱,也即返回值類型

經過method.getParameterTypes()獲取方法各參數的Class對象組成的數組

MyClass.java:
public class MyClass {
  public void method1 (int a, long b) {   };
  public void method2 (float a, double b) {   };
  public void method3 (String str) {   };
}
Test.java:
複製代碼
public class Test {
  public static void printMethodsMessage (Object obj) {
    Class c = obj.getClass();  // 取得obj所屬類對應的Class對象
    Method [] methods = c.getDeclaredMethods(); // 取得obj所屬類中方法對應的Method對象組成的數組
    for (Method method : methods) { // 遍歷Method對象
      String methodName = method.getName();   // 取得方法名
      String paramsStr = "";  // 用於存放某個方法參數類型列表的字符串
      Class [] paramsClasses = method.getParameterTypes();
      for (Class pc: paramsClasses) {
        String paramStr = pc.getName(); // 獲取當前參數類型
        paramsStr+=paramStr + "  ";
      }
      System.out.println(methodName+ "方法的全部參數的類型列表:" + paramsStr);
    }
  }
  public static void main(String [] args) {     MyClass myClass = new MyClass();     printMethodsMessage(myClass);   } }
複製代碼
運行結果:
method2方法的參數類型列表:float  double 
method1方法的參數類型列表:int  long 
method3方法的參數類型列表:java.lang.String 

利用反射API分析類中成員變量信息

獲取成員變量類型對應的的Class對象

讀取成員變量的值
 
MyClass.java:
public class MyClass {
  private int number = 123;
  private String name ="彭湖灣";
}
Test.java:
複製代碼
import java.lang.reflect.Field;
import java.lang.reflect.Method;
 
public class Test {
  public static void printFieldsMessage (Object obj) {
    Class c = obj.getClass();  // 取得obj所屬類對應的Class對象
    try {
      Field field = c.getDeclaredField("name");  // 取得名稱爲name的field對象
      field.setAccessible(true); // 這一步很重要!!!設置爲true才能訪問私有成員變量name的值!
      String nameValue = (String) field.get(obj); // 獲取obj中name成員變量的值
      System.out.println("MyClass類中name成員變量的值爲:" + nameValue);  // 輸出
    } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
       // TODO Auto-generated catch block
        e.printStackTrace();
    }
  }
    
public static void main(String [] args) {   MyClass myClass = new MyClass();     printFieldsMessage(myClass);   }
}
複製代碼
運行結果:
MyClass類中name成員變量的值爲:彭湖灣

 經過getType方法讀取成員變量類型的Class對象

這裏只展示比較關鍵的兩段代碼:
Field field = class1.getDeclaredField(number");
System.out.print(field.getType().getName());
運行結果:
int
【注意】:由於java權限的緣由,直接讀取私有成員變量的值是非法的(加了field.setAccessible(true)後就能夠了),但仍能夠直接讀取私有成員變量的類型

利用反射API分析類中構造器信息

分析構造函數的時候,其實思路大致上和分析方法時候一致,關鍵在於獲取參數所屬類的Class對象
 
區別在於:
 
1. 獲取該類聲明的構造器用的是getDeclaredConstructors方法而不是getDeclaredMethods方法
2. 構造函數沒有返回值,因此不須要分析返回值(我彷佛說了廢話....)
 
廢話很少說了,看下面的例子:
 
MyClass.java:
public class MyClass {
  public MyClass(int a, String str){}
}
Test.java:
複製代碼
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
 
public class Test {
  public static void printContructorsMessage (Object obj) {
    Class c = obj.getClass();  // 取得obj所屬類對應的Class對象
    Constructor [] constructors = c.getDeclaredConstructors();
    for (Constructor constructor : constructors) {
      Class [] paramsClasses =  constructor.getParameterTypes();
      String paramsStr = "";
      for (Class pc : paramsClasses) {
        String paramStr = pc.getName();
        paramsStr+=paramStr + "  ";
      }
      System.out.println("構造函數的全部參數的類型列表:" + paramsStr);
    }
  }
  public static void main(String [] args) {
    MyClass myClass = new MyClass(1, "彭湖灣");
    printContructorsMessage(myClass);
  }
}
複製代碼
運行結果:
構造函數的全部參數的類型列表:int  java.lang.String 

利用反射動態加載類,並用該類建立實例對象

動態加載類
 
咱們用普通的方式使用一個類的時候,類是靜態加載的
而使用Class.forName("XXX")這種方式,則屬於動態加載一個類
 
靜態加載的類在編譯的時候就能肯定該類是否存在,但動態加載一個類的時候卻沒法在編譯階段肯定是否存在該類,而是在運行時候纔可以肯定是否有這個類,因此要捕捉可能發生的異常
 
咱們能夠從eclipse的使用上有個相對直觀的瞭解:
 
eclipse在保存的時候是能夠自動編譯的,SO
 
例如咱們若是直接使用一個本就不存在的類NotExistClass的時候
 

 

保存後,在編譯階段就可以發現:「誒? 好像沒有這個類哦!」
報的錯誤是:
 
NotExistClass cannot be resolved to a type
可是若是咱們用Class.forName("XXX")動態加載一個類呢?
咱們發現,保存後,在編譯階段已經不能發現這個錯誤了,對應的是要捕捉可能發生的異常
 
 
用該動態加載的類建立實例對象
 
Class對象有一個newInstance方法,咱們能夠用它來建立實例對象
Class classInstance = Class.forName("mypackage.MyClass");
MyClass myClass = (MyClass) classInstance.newInstance();

不過要注意的是,由於newInstance返回的是一個Object,因此要作強制類型轉換,將其變成MyClass類型函數

捕捉可能產生的異常後:
複製代碼
public class Test {
   public static void main(String [] args) {
     try {
       Class classInstance = Class.forName("mypackage.MyClass");
       MyClass myClass = (MyClass) classInstance.newInstance();
     } catch (ClassNotFoundException e) {
       // TODO Auto-generated catch block
       e.printStackTrace();
     } catch (InstantiationException e) {
       // TODO Auto-generated catch block
       e.printStackTrace();
     } catch (IllegalAccessException e) {
       // TODO Auto-generated catch block
       e.printStackTrace();
     }
   }
}
複製代碼
全文的總結
 
若是您已經看到這裏了,那麼請容許我在用多那麼一點羅嗦的文字作個總結:
 
總結
 
1.反射爲咱們提供了全面的分析類信息的能力,例如類的方法,成員變量和構造器等的相關信息,反射可以讓咱們很方便的獲取這些信息, 而實現這個獲取過程的關鍵是取得類的Class對象,而後根據Class對象取得相應的Method對象,Field對象和Constructor對象,再分別根據各自的API取得信息。
2.反射還爲咱們提供動態加載類的能力
 
一些細節
1. API中getDeclaredXXX和getXXX的區別在於前者只獲取本類聲明的XXX(如成員變量或方法),而不獲取超類中繼承的XXX, 後者相反
2. API中, getXXXs(注意後面的s)返回的是一個數組, 而對應的 getXXX("鍵")按鍵獲取一個值(這個時候由於可能報已檢查異常因此要用try-catch語句包裹)
3. 私有成員變量是不能直接獲取到值的!由於java自己的保護機制,容許你取得私有成員變量的類型,可是不容許直接獲取值,因此要對對應的field對象調用field.setAccessible(true) 放開權限
 
 
參考:利用反射實現JavaBean的自動賦值 
import java.lang.reflect.Method; 
import java.util.*; 
import javax.servlet.http.HttpServletRequest; 
import com.sns.exception.ApplicationException; 
public final class ParameterUtil { 
public static void setFormBean(HttpServletRequest request, Object bean) { 
Class c = bean.getClass(); 
Method[] ms = c.getMethods(); 
for(int i=0; i<ms.length; i++) { 
    String name = ms.getName(); 
    if(name.startsWith("set")) { 
     Class[] cc = ms.getParameterTypes(); 
     if(cc.length==1) { 
      String type = cc[0].getName(); // parameter type 
      try { 
        // get property name: 
        String prop = Character.toLowerCase(name.charAt(3)) + name.substring(4); 
        // get parameter value: 
        String param = getString(request, prop); 
        if(param!=null && !param.equals("")) { 
         //ms.setAccessible(true); 
         if(type.equals("java.lang.String")) { 
          ms.invoke(bean, new Object[] {param}); 
         } 
         else if(type.equals("int") || type.equals("java.lang.Integer")) { 
          ms.invoke(bean, new Object[] {new Integer(param)}); 
         } 
         else if(type.equals("long") || type.equals("java.lang.Long")) { 
          ms.invoke(bean, new Object[] {new Long(param)}); 
         } 
         else if(type.equals("boolean") || type.equals("java.lang.Boolean")) { 
          ms.invoke(bean, new Object[] { Boolean.valueOf(param) }); 
         } 
         else if(type.equals("java.util.Date")) { 
          Date date = DateUtil.parseDateTime(param); 
          if(date!=null) 
            ms.invoke(bean, new Object[] {date}); 
         } 
        } 
      } 
      catch(Exception e) { 
        e.printStackTrace(); 
      } 
     } 
    } 
} 
} 
} 
每當發現JavaBean中的setXxx()方法時,便自動尋找表單的對應字段xxx,若是找到,就利用反射調用此方法,將對應的字段值賦給JavaBean。 
因爲表單傳遞的變量名和值所有是字符串,所以須要作某些轉換。目前,該程序能處理的數據類型包括:boolean,Boolean,int,Integer,long,Long,Date,不被支持的數據類型被自動忽略。你也能夠很方便地添加新的類型。 
請 注意,只有public的set方法可以被調用。若是你但願private或protected的set方法也能被調用,請將紅色標識的 getMethods()改成getDeclaredMethods(),以便得到包括private和protected在內的全部方法,並將 ms.setAccessible(true);的註釋去掉,以便能正確調用private和protected方法。 
反射是Java語言很是強大的功能,可是因爲反射會破壞對象封裝,而且反射調用的速度較慢,所以,只能在必要的工具類中使用。
 
 
 
參考資料:
《java核心技術 卷1》—— Cay S. Horstmann, Gary Cornell
相關文章
相關標籤/搜索