目錄
並不是所有的Class都能在編譯時明確,因此在某些情況下需要在運行時再發現和確定類型信息(比如:基於構建編程,),這就是RTTI(Runtime Type Information,運行時類型信息)。
在java中,有兩種RTTI的方式,一種是傳統的,即假設在編譯時已經知道了所有的類型;還有一種,是利用反射機制,在運行時再嘗試確定類型信息。
本文主要講反射方式實現的RTTI,建議在閱讀本文之前,先了解類的加載機制(參考我的博客:Java系列筆記(1) - Java 類加載與初始化)。
在本文中,將共同使用下面的玩具類Toy,該類中定義了公有、私有方法,變量,構造方法,父類、父接口等:
package myblog.rtti; /** * @project MyBlog * @create 2013年6月28日 下午4:42:46 * @version 1.0.0 * @author 張廣 */ public interface IToy { public String playToy(String player) throws Exception; }
package myblog.rtti; public class AbstractToy implements IToy { @Override public String playToy(String player) throws Exception { System.out.println(player + " plays abstract toy"); return ""; } }
package myblog.rtti; public class Toy extends AbstractToy { private String name; public String color; protected int size; public static final int price = 10; static { System.out.println("Loading"); } public Toy() {// 構造方法一定要聲明為public類型,不然用getConstructors無法得到 System.out.println("Initialing"); setName("myToy"); color = "red"; size = 5; } public Toy(String name, String color, int size) { this.setName(name); this.color = color; this.size = size; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String playToy(String player) throws Exception { String msg = buildMsg(player); System.out.println(msg); return msg; } private String buildMsg(String player) { String msg = player + " plays " + name; return msg; } }
嚴格的說,反射也是一種形式的RTTI,不過,一般的文檔資料中把RTTI和反射分開,因為一般的,大家認為RTTI指的是傳統的RTTI,通過繼承和多態來實現,在運行時通過調用超類的方法來實現具體的功能(超類會自動實例化為子類,或使用instance of)。
傳統的RTTI有3種實現方式:
- 向上轉型或向下轉型(upcasting and downcasting),在java中,向下轉型(父類轉成子類)需要強制類型轉換
- Class對象(用了Class對象,不代表就是反射,如果只是用Class對象cast成指定的類,那就還是傳統的RTTI)
- instanceof或isInstance()
傳統的RTTI與反射最主要的區別,在於RTTI在編譯期需要.class文件,而反射不需要。傳統的RTTI使用轉型或Instance形式實現,但都需要指定要轉型的類型,比如:
public void rtti(Object obj){ Toy toy = Toy(obj); // Toy toy = Class.forName("myblog.rtti.Toy") // obj instanceof Toy }
注意其中的obj雖然是被轉型了,但在編譯期,就需要知道要轉成的類型Toy,也就是需要Toy的.class文件。
相對的,反射完全在運行時在通過Class類來確定類型,不需要提前加載Toy的.class文件。
那到底什么是反射(Reflection)呢?反射有時候也被稱為內省(Introspection),事實上,反射,就是一種內省的方式,Java不允許在運行時改變程序結構或類型變量的結構,但它允許在運行時去探知、加載、調用在編譯期完全未知的class,可以在運行時加載該class,生成實例對象(instance object),調用method,或對field賦值。這種類似於“看透”了class的特性被稱為反射(Reflection),我們可以將反射直接理解為:可以看到自己在水中的倒影,這種操作與直接操作源代碼效果相同,但靈活性高得多。
關於Java的反射API,沒必要去記憶,可以在任何JDK API中查詢即可:
Class類:http://www.ostools.net/uploads/apidocs/jdk-zh/java/lang/Class.html
reflect包:http://www.ostools.net/uploads/apidocs/jdk-zh/java/lang/reflect/package-summary.html
package myblog.rtti; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; /** * @project MyBlog * @create 2013年6月28日 下午3:00:33 * @version 1.0.0 * @author 張廣 */ public class ToyReflection { public static void printInfo(String info, Object obj) { if (obj.getClass().isArray()) { System.out.println(info + ": "); int length = Array.getLength(obj); System.out.println("Array Size: " + length); for (int i = 0; i < length; i++) { System.out.print("Array[" + i + "]: " + Array.get(obj, i) + ", "); } if (length != 0) System.out.println(); } System.out.println(info + ": " + obj.toString()); } public static void main(String[] args) { try { // 獲得類對象 Class<?> c = Class.forName("myblog.rtti.Toy"); printInfo("獲得類對象", c); // 獲得超類 Class<?> superClass = c.getSuperclass(); printInfo("獲得超類", superClass); // 獲得所有父接口 Class<?>[] interfaces = c.getInterfaces(); printInfo("獲得所有父接口", interfaces); // 實例化 Toy toy = (Toy) c.newInstance(); printInfo("實例化", toy); // 獲得訪問屬性為public的構造方法 Constructor<?>[] constructors = c.getConstructors(); printInfo("獲得構造方法", constructors); // 獲得指定參數的構造方法 Constructor<?> constructor = c.getDeclaredConstructor(String.class, String.class, int.class); printInfo("獲得指定構造方法", constructor); // 獲得方法,getMethod只能獲得public方法,包括父類和接口繼承的方法 Method method = c.getMethod("playToy", String.class); printInfo("獲得公有方法", method); // 調用方法 method.invoke(toy, "張三"); // 獲得修飾符,包括private/public/protect,static String modifier = Modifier.toString(method.getModifiers()); printInfo("獲得修飾符", modifier); // 獲得參數類型 Class<?>[] paramTypes = method.getParameterTypes(); printInfo("獲得參數類型", paramTypes); // 獲得返回值類型 Class<?> returnType = method.getReturnType(); printInfo("獲得返回值類型", returnType); // 獲得異常類型 Class<?>[] excepTypes = method.getExceptionTypes(); printInfo("獲得異常類型", excepTypes); // 調用私有方法,getDeclaredMethod獲得類自身的方法,包括public,protect,private方法 Method method2 = c.getDeclaredMethod("buildMsg", String.class); method2.setAccessible(true); String result = (String) method2.invoke(toy, "李四"); printInfo("獲得私有方法", result); // 獲得全部屬性 Field[] fields = c.getFields(); printInfo("獲得全部屬性", fields); // 獲得類自身定義的指定屬性 Field field = c.getDeclaredField("name"); printInfo("獲得自身屬性", field); // 獲得類及其父類,父接口定義的public屬性 Field field2 = c.getField("color"); printInfo("獲得公有屬性", field2); // 獲得權限修飾符,包括private/public/protect,static,final String fieldModifier = Modifier.toString(field.getModifiers()); printInfo("獲得權限修飾符", fieldModifier); // 操作數組 int[] exampleArray = { 1, 2, 3, 4, 5 }; // 獲得數組類型 Class<?> componentType = exampleArray.getClass().getComponentType(); printInfo("數組類型", componentType.getName()); // 獲得長度 printInfo("數組長度", Array.getLength(exampleArray)); // 獲得指定元素 printInfo("獲得數組元素", Array.get(exampleArray, 2)); // 修改指定元素 Array.set(exampleArray, 2, 6); printInfo("修改數組元素", exampleArray); // 獲得當前的類加載器 printInfo("獲得當前類加載器", toy.getClass().getClassLoader().getClass().getName()); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } } }
通過上面的代碼,可以清晰的理解如何“在水中看到自己”,不過需要注意的有幾點:
1,在java的反射機制中,getDeclaredMethod得到的是全部方法,getMethod得到的是公有方法;
2,反射機制的setAccessible可能會破壞封裝性,可以任意訪問私有方法和私有變量;
3,setAccessible並不是將private改為public,事實上,public方法的accessible屬性也是false的,setAccessible只是取消了安全訪問控制檢查,所以通過設置setAccessible,可以跳過訪問控制檢查,執行的效率也比較高。參考:http://blog.csdn.net/devilkin64/article/details/7766792
反射機制給予Java開發很大的靈活性,但反射機制本身也有缺點,代表性的缺陷就是反射的性能,一般來說,通過反射調用方法的效率比直接調用的效率要至少慢一倍以上。
關於性能的問題,可以參考這篇博客http://blog.csdn.net/l_serein/article/details/6219897
反射的一個很重要的作用,就是在設計模式中的應用,包括在工廠模式和代理模式中的應用。關於這一方面,我會在后續的文章中介紹,有興趣的朋友也先可以參考這篇文章http://www.cnblogs.com/rollenholt/archive/2011/09/02/2163758.html中關於動態代理模式實現方法的文章。
參考資料
JAVA編程思想,第14章
Java-RTTI與反射機制--詳細 :http://blog.csdn.net/dahaizisheng/article/details/1762327
Java反射詳解:http://www.cnblogs.com/rollenholt/archive/2011/09/02/2163758.html
RTTI和反射機制:http://blog.sina.com.cn/s/blog_5ea2d6840100v9bu.html
Java中的RTTI和反射機制:http://blog.csdn.net/a81895898/article/details/8457623
Java反射性能測試:http://blog.csdn.net/l_serein/article/details/6219897
