原文:
http://today.java.net/pub/a/today/2008/02/12/reflection-in-action.html
你曾經為IDE會自動的列舉出所有你所編寫的類的詳情,甚至連私有的字段和方法也“難逃魔掌”而感到驚訝嗎?此外,這些IDE居然還能夠識別那些并不提供源碼并壓縮成JAR文件的類。它們是怎么做到的?
這些都是因為反射。
本文將通過逐步列舉一個類的內容,來闡明反射是如何被用來“撬動”編程的。同時逐步形成高級別的抽象。我們將會從一個十分簡單的例子開始,并一步步地在一個程序中實施反射。
什么是反射?
反射是一種機制,它允許動態發現和綁定類、方法、字段,以及所有其他的由語言所產生的元素。反射可以做的不僅僅是簡單地列舉類、字段以及方法。通過反射,我們可以還能夠在需要時完成創建實例、調用方法以及訪問字段的工作。
大多數程序員曾使用過動態類載入技術來載入他們的JDBC驅動。這種載入方法類似于下面這一段載入JDBC驅動實例的代碼片段:
Class.forName("com.mysql.jdbc.Driver").newInstance();
為何與何時使用反射?
反射提供了一個高級別的抽象,換句話說,反射允許我們在程序運行時對手頭上的對象進行檢查并運行。舉個例子,想像一下,當你在執行那相同的任務時——如像上面的例子那樣在若干的對象中查找一個實例,你可以選擇為每一個不同的對象寫相同的代碼,也可以使用反射來完成這項任務。或許你已經開始意識到了,反射可以減少近似的代碼的維護量。因為使用了反射,你的實例查找代碼將會對其他類起作用。我們稍后將會演示這個例子。我已經將它加入到這篇文章里,以便向你展示我們如何從反射中得利。
動態發現
下面我們以發現一個類的內容并列出它的構造子,字段,方法作為開始吧。這并不實用,但它能讓我們直觀地抓住反射的原理及其了解其API。
創建一個Product類,如下所示。所有我們的例子都將放到了相同的package里,叫ria。
package ria;
public class Product {
private String description;
private long id;
private String name;
private double price;
//省略若干Getter與Setter
}
創建好Product類后,我們下面繼續創建第二個類,叫ReflectionUtil。它將列舉出第一個類(Product)的詳情?;蛟S你已經預料到了,這個類會包含一些實用的方法,它們將完成這個程序所需要的反射功能。目前,這個類只會包含一個方法,describeInstance(Object),它需要一個類型為Object的參數。
類ReflectionUtil的代碼如下所示。
package ria;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectionUtil {
public static void describeInstance(Object object) {
Class<?> clazz = object.getClass();
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
Field[] fields = clazz.getDeclaredFields();
Method[] methods = clazz.getDeclaredMethods();
System.out.println("Description for class: " + clazz.getName());
System.out.println();
System.out.println("Summary");
System.out.println("-----------------------------------------");
System.out.println("Constructors: " + (constructors.length));
System.out.println("Fields: " + (fields.length));
System.out.println("Methods: " + (methods.length));
System.out.println();
System.out.println();
System.out.println("Details");
System.out.println("-----------------------------------------");
if (constructors.length > 0) {
System.out.println();
System.out.println("Constructors:");
for (Constructor<?> constructor : constructors) {
System.out.println(constructor);
}
}
if (fields.length > 0) {
System.out.println();
System.out.println("Fields:");
for (Field field : fields) {
System.out.println(field);
}
}
if (methods.length > 0) {
System.out.println();
System.out.println("Methods:");
for (Method method : methods) {
System.out.println(method);
}
}
}
}
Java包含了一組反射相關的類,它們打包進了反射API(Reflection API)中。構造子類(Constructor)、字段類(Field)以及方法類(Method)便是其中的一部分。如同眾所周知的Class類一樣,它們在Java中被用來在程序中描述對象。為了描述一個對象,我們需要知道這個對象是由什么組成的。我們如何開始呢?那就從類開始吧,它包含了我們所有的代碼。
Class<?> clazz = object.getClass();
注意這里的泛型聲明Class<?>。泛型,簡單地說,就是通過限定給出的實例是某種類型的,從而提供類型安全(type-safe)的操作。我們的方法(describeInstance(Object))并不綁定到一個特定類型上,它被設計為對任意給出的對象都能正常運行。因此,那無限制的通配符,<?>,將會被使用到。
Class類有一些方法,下面我們將集中于那些對我們有用的方法上。下面的代碼片段中列出了這些方法。
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
Field[] fields = clazz.getDeclaredFields();
Method[] methods = clazz.getDeclaredMethods();
上面的這些來自Class類的方法返回了一組構造子、字段、方法,這是它們組成了這個對象。
注意那個Class類含有兩組getter方法:一組在它們的名字中包含了declared單詞,而另一組沒有。不同之處在于,getDeclaredMethods()會返回所有屬于這個類的方法,而getMethods()則只返回聲明為public的方法。這對于理解為何只有在這個類中聲明的方法才予以返回的原因同樣重要。繼承的方法是不會被檢索到的。
了解ReflectionUtil類并沒有對一個關于Product類的引用十分重要。我們需要另一個類來創建一個Product實例并打印出它的詳情。
package ria;
public class Main {
public static void main(String[] args) throws Exception {
Product product = new Product();
product.setId(300);
product.setName("My Java Product Name");
product.setDescription("My Java Product description
");
product.setPrice(10.10);
ReflectionUtil.describeInstance(product);
}
}
上面的這個類運行后應該能輸出下面的這段信息(或一些近似的信息):
Description for class: ria.Product
Summary
-----------------------------------------
Constructors: 1
Fields: 4
Methods: 8
Details
-----------------------------------------
Constructors:
public ria.Product()
Fields:
private java.lang.String ria.Product.description
private long ria.Product.id
private java.lang.String ria.Product.name
private double ria.Product.price
Methods:
public java.lang.String ria.Product.getName()
public long ria.Product.getId()
public void ria.Product.setName(java.lang.String)
public void ria.Product.setId(long)
public void ria.Product.setDescription(java.lang.String)
public void ria.Product.setPrice(double)
public java.lang.String ria.Product.getDescription()
public double ria.Product.getPrice()
為了讓這個方法更加有用,我們可以加入打印出這個實例中定義的字段的值的功能。Field類有一個叫get(Object)的方法,它返回給定的實例的相應字段的值。
現在就以我們的Procuct類來舉個例子吧。這個類有四個實例變量。由于獲取的值依賴于實例,因此不同的實例可能有不同的值。所以,必須向Field提供實例作為參數輸入,這樣我們才能夠獲取這個實例的對應的字段的值。如下所示:
field.get(object)
這里的field是Field的一個實例,同時object是為一個任意Java類的實例。
在我們草率地開始增加代碼前,我們必須認識到這么一個事實,那就是類的字段的私有訪問性是可以修改的。如果我們調用一個標記為private的字段的Field類的get(Object)方法,這樣會拋出一個異常。因此,我們需要在著手訪問那個字段的值前,調用這個Field類的方法setAccessible(boolean),并傳遞true作為參數進去。
field.setAccessible(true);
現在,我們知道了所有獲取字段的值的相關小技巧了,我們可以在剛才那個
decribeInstance(Object)方法下面
接著增加如下的代碼了。
if (fields.length > 0) {
System.out.println();
System.out.println();
System.out.println("Fields' values");
System.out.println("-----------------------------------------");
for (Field field : fields) {
System.out.print(field.getName());
System.out.print(" = ");
try {
field.setAccessible(true);
System.out.println(field.get(object));
} catch (IllegalAccessException e) {
System.out.println("(Exception Thrown: " + e + ")");
}
}
}
為了給你顯示這段代碼的威力,我來創建一個
java.awt.Rectangle類的實例吧,用這段增強版的
describeInstance(Object)
代碼來打印出這個實例的
詳情。
Rectangle rectangle = new Rectangle(1, 2, 100, 200);
ReflectionUtil.describeInstance(rectangle);
上面的這個代碼片段應該能輸出一些類似下面的這些信息。提示一下,由于顯示的信息過長,部分信息被省略掉了。
Description for class: java.awt.Rectangle
Summary
-----------------------------------------
Constructors: 7
Fields: 5
Methods: 39
Details
-----------------------------------------
Constructors:
public java.awt.Rectangle()
public java.awt.Rectangle(java.awt.Rectangle)
public java.awt.Rectangle(int,int,int,int)
public java.awt.Rectangle(int,int)
public java.awt.Rectangle(java.awt.Point,java.awt.Dimension)
public java.awt.Rectangle(java.awt.Point)
public java.awt.Rectangle(java.awt.Dimension)
Fields:
public int java.awt.Rectangle.x
public int java.awt.Rectangle.y
public int java.awt.Rectangle.width
public int java.awt.Rectangle.height
private static final long java.awt.Rectangle.serialVersionUID
Methods:
public void java.awt.Rectangle.add(int,int)
public void java.awt.Rectangle.add(java.awt.Point)
public void java.awt.Rectangle.add(java.awt.Rectangle)
public boolean java.awt.Rectangle.equals(java.lang.Object)
public java.lang.String java.awt.Rectangle.toString()
public boolean java.awt.Rectangle.contains(int,int,int,int)
public boolean java.awt.Rectangle.contains(java.awt.Rectangle)
public boolean java.awt.Rectangle.contains(int,int)
public boolean java.awt.Rectangle.contains(java.awt.Point)
public boolean java.awt.Rectangle.isEmpty()
public java.awt.Point java.awt.Rectangle.getLocation()
public java.awt.Dimension java.awt.Rectangle.getSize()
public void java.awt.Rectangle.setSize(java.awt.Dimension)
public void java.awt.Rectangle.setSize(int,int)
public void java.awt.Rectangle.resize(int,int)
private static native void java.awt.Rectangle.initIDs()
public void java.awt.Rectangle.grow(int,int)
public boolean java.awt.Rectangle.intersects(java.awt.Rectangle)
private static int java.awt.Rectangle.clip(double,boolean)
public java.awt.geom.Rectangle2D java.awt.Rectangle.createIntersection(java
.
public java.awt.geom.Rectangle2D java.awt.Rectangle.createUnion(java.awt.geo
public java.awt.Rectangle java.awt.Rectangle.getBounds()
public java.awt.geom.Rectangle2D java.awt.Rectangle.getBounds2D()
public double java.awt.Rectangle.getHeight()
public double java.awt.Rectangle.getWidth()
public double java.awt.Rectangle.getX()
public double java.awt.Rectangle.getY()
public boolean java.awt.Rectangle.inside(int,int)
public java.awt.Rectangle java.awt.Rectangle.intersection(java.awt.Rectangle)
public void java.awt.Rectangle.move(int,int)
public int java.awt.Rectangle.outcode(double,double)
public void java.awt.Rectangle.reshape(int,int,int,int)
public void java.awt.Rectangle.setBounds(int,int,int,int)
public void java.awt.Rectangle.setBounds(java.awt.Rectangle)
public void java.awt.Rectangle.setLocation(java.awt.Point)
public void java.awt.Rectangle.setLocation(int,int)
public void java.awt.Rectangle.setRect(double,double,double,double)
public void java.awt.Rectangle.translate(int,int)
public java.awt.Rectangle java.awt.Rectangle.union(java.awt.Rectangle)
Fields' values
-----------------------------------------
x = 1
y = 2
width = 100
height = 200
serialVersionUID = -4345857070255674764
創建一個新的使用反射的實例
反射可以用來創建一個對象的實例。關于動態創建對象的實例有許多例子,如前面所說的動態載入一個JDBC驅動。更進一步,我們可以使用構造子(Constructor
)類來創建新實例,特別是那些實例化時需要參數的實例。將下面的兩個重載的方法加入到我們的ReflectionUtil中。
public static <T> T newInstance(Class<T> clazz)
throws IllegalArgumentException, SecurityException,
InstantiationException, IllegalAccessException,
InvocationTargetException, NoSuchMethodException {
return newInstance(clazz, new Class[0], new Object[0]);
}
public static <T> T newInstance(Class<T> clazz, Class<?>[] paramClazzes,
Object[] params) throws IllegalArgumentException,
SecurityException, InstantiationException, IllegalAccessException,
InvocationTargetException, NoSuchMethodException {
return clazz.getConstructor(paramClazzes).newInstance(params);
}
注意,方法newInstance(Object[])在構造子參數不匹配的時候可能會拋出一個異常。被實例化的類必須包含一個給定簽名的構造子。
第一個方法(newInstance(Class<T>))可以用來實例化任何一個擁有默認構造子的類的對象。否則可以使用第二個方法:傳遞參數的類型以及其值,當匹配上對應的構造子后就會執行相應的實例化。舉個例子,類Rectangle就可以通過使用含有四個int類型的參數的構造子來進行實例化。使用的代碼如下所示:
Object[] params = { 1, 2, 100, 200 };
Class[] paramClazzes = { int.class, int.class, int.class, int.class };
Rectangle rectangle = ReflectionUtil.newInstance(
Rectangle.class, paramClazzes, params);
System.out.println(rectangle);
上面的這段代碼將產生如下的輸出。
java.awt.Rectangle[x=1,y=2,width=100,height=200]
通過反射改變字段的值
字段的值可以使用反射來進行設置,方法類似于如何使用反射來讀取它們。需要注意的是在設置這些字段的值前要設置它們的可訪問性,不然就會拋出異常的。
field.setAccessible(true);
field.set(object, newValue);
我們可以十分容易地編寫出一個方法,用它來設置任何對象的值,就如同下面列出的例子一般。
public static void setFieldValue(Object object, String fieldName,
Object newValue) throws NoSuchFieldException,
IllegalArgumentException, IllegalAccessException {
Class<?> clazz = object.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, newValue);
}
這個方法有一個不足,它只能檢索出這個類定義的字段;繼承而來的字段并不包括在內。這個問題可以通過下面的這個方法來得到解決,它將向這個對象的基類查詢相應的Field類。
public static Field getDeclaredField(Object object, String name)
throws NoSuchFieldException {
Field field = null;
Class<?> clazz = object.getClass();
do {
try {
field = clazz.getDeclaredField(name);
} catch (Exception e) { }
} while (field == null & (clazz = clazz.getSuperclass()) != null);
if (field == null) {
throw new NoSuchFieldException();
}
return field;
}
這個方法將返回給定名字的Field對象,如果找到的話;否則,就會拋出一個異常以提示這個類以及它的基類都沒有這個字段。這個方法從給定的類開始查找,逐步向上地查找各個基類直到每一個Field都被遍歷過。當然,也有可能沒有基類。
提示,所有的Java類都是繼承于Object類。正如你所認識到的,Object類并不繼承自己。所以Object類沒有基類。
將前文提到的方法setFieldValue(Object, String, Object)進行修改一下,以滿足這種情況。修改如下黑體所示。
public static void setFieldValue(Object object, String fieldName,
Object newValue) throws IllegalArgumentException,
IllegalAccessException, NoSuchFieldException {
Field field = getDeclaredField(object, fieldName);
field.setAccessible(true);
field.set(object, newValue);
}
下面來創建另一個叫做Book的類,它泛化于前面所討論過的Product類。我們將在此應用我們剛才所學習到的關于反射的知識。
package ria;
public class Book extends Product {
private String isbn;
//Getters and setters are omitted for shortness
}
現在我們用方法setFieldValue(Object, String, Object)來設置book的id值。
Book book = new Book();
ReflectionUtil.setFieldValue(book, "id", 1234L);
System.out.println(book.getId());
上面這段代碼將輸出1234。
通過反射調用方法
你或許已經猜到了,調用方法更上面提到的創建實例以及訪問字段的方法是十分相似的。
就反射而言,所有的方法都需要參數并會返回一個值。這或許聽起來有點怪異,但事實就是這樣。好吧,來看看下面的這個方法:
public void doNothing(){
// This method doesn't do anything
}
這個方法會返回一個void類型的值,同時它有一個空的參數列表。這個方法能夠通過下面的方式,用反射來進行調用。
Class<?> clazz = object.getClass();
Method method = Clazz.getDeclaredMethod("doNothing");
method.invoke(object, new Object[0]);
那個Method類的方法invoke,需要兩個參數變量:被調用方法的對象實例,以及排成了一個數組的參數列表。注意那個doNothing()方法是沒有參數的。就算如此,我們還是需要為它設置一個長度為0的數組作為參數變量。
在我們的例子中,方法執行后同樣是要返回的。如果這個方法確實有返回值的話,那么這回返回值將會以一個Object的形式進行返回,就如同下面的例子一樣。
Object returnValue = method.invoke(object, new Object[0]);
在上面的這個例子中,由于這個方法本身是不返回任何東西的,因此invoke執行后返回的值是個null。注意,這里返回null值可能會產生一個小迷惑。有時我們出于某種目的,方法會被設計成返回null的。
在結束本節前,需要強調的一點是要知道方法會如同字段一樣是有繼承性的。于是,我們就來創建一個有效的方法,用它來檢索出那些繼承而來的方法。
public static Method getDeclaredMethod(Object object, String name)
throws NoSuchMethodException {
Method method = null;
Class<?> clazz = object.getClass();
do {
try {
method = clazz.getDeclaredMethod(name);
} catch (Exception e) { }
} while (method == null & (clazz = clazz.getSuperclass()) != null);
if (method == null) {
throw new NoSuchMethodException();
}
return method;
}
最后,我們得出了一個通用的invoke方法。再次注意一下,方法可能會被定位為私有,因此需要在調用它們之前設置一下它們的可訪問性。
public static Object invokeMethod(Object object, String methodName,
Object[] arguments) throws NoSuchMethodException,
IllegalArgumentException, IllegalAccessException,
InvocationTargetException {
Method method = getDeclaredMethod(object, methodName);
method.setAccessible(true);
return method.invoke(object, arguments);
}
除了可以調用執行那些私有方法外,通過反射來調用方法能很好地獲知類的功能并在運行時中十分容易地改變它的執行流程。
反射應用程序
直到現在,我們都僅僅是創建了一些工具方法并試驗了一些簡單的例子。但現實中的程序要做的遠不止這些。
Until now, we have only created utility methods and tried out simple examples. Real-life programming requires more than that. Imagine that we need to search through our objects and determine whether a given object matches some criteria or not. The first option is to write an interface and implement it in every object that returns true if this instance matches the criteria, false otherwise. Unfortunately, this approach requires us to implement a method within every class we have. New classes will have to implement this interface and provide a body for its abstract method. Alternatively, we can use reflection to retrieve the object's fields and check whether their values meet the criteria.
Let us first create another method that returns the object's fields. Remember that there's no built-in method that returns all the fields including the inherited ones. Thus we need to retrieve them ourselves by extracting them set by set until we reach the top of the hierarchy. This method can be added to the ReflectionUtil class.
累死了!
未完待續。。。。