程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java反射實踐

Java反射實踐

編輯:關於JAVA
 

 您是否考慮過這些問題: IDE 如何列出類的所有詳細信息,包括私有字段和私有方法? IDE 還能夠列出 JAR 文件中的類(及其詳細信息),它們是如何做到的?

下面是反射的一些例子。

本文將闡述如何在編程中應用反射,以及如何在高級抽象中應用反射。我們將從一個十分簡單的例子入手,然後創建一個簡單的程序來使用反射。

什麼是反射?
反射是一種機制,它允許動態地發現和綁定類、方法、字段,以及由語言組成的所有其他元素。列出類、字段和方法只是反射的基本應用。通過反射,我們實際上還能夠在需要時創建實例、調用方法以及訪問字段。

大多數程序員曾使用過動態類載入技術來載入他們的 JDBC 驅動程序。這種載入方法類似於下面這一段動態載入 MySQL JDBC 驅動程序實例的代碼片段:

Class.forName("com.mysql.jdbc.Driver").newInstance();
使用反射的原因和時機
反射提供了一個高級別的抽象。換句話說,反射允許我們在運行時對手頭上的對象進行檢查並進行相應的操作。例如,如果您必須在多種對象上執行相同的任 務,如搜索某個實例。則可以為每種不同的對象編寫一些代碼,也可以使用反射。或許您已經意識到了,反射可以減少近似代碼的維護量。因為使用了反射,您的實 例搜索代碼將會對其他類起作用。我們稍後會談到這個示例。我已經將它加入到這篇文章裡,以便向您展示我們如何從反射中獲益。

動態發現
下面我們從發現一個類的內容並列出它的構造、字段、方法開始。這並不實用,但它能讓我們直觀地了解反射 API 的原理及其他內容。

創建 Product 類,如下所示。我們的所有示例都保存在名為 ria 的程序包中。

package ria;public class Product {private String description;private long id;private String name;private double price;//Getters and setters are omitted for shortness}
創建好 Product 類後,我們下面繼續創建第二個類,名為 ReflectionUtil,它將列出第一個類的 (Product) 詳細信息。或許您已經預料到了,這個類會包含一些實用的方法,這些方法將執行這個應用程序中所需的所有反射功能。目前,這個類將只包含一個方法 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 下。類 Constructor、 Field 和 Method 就是屬於這個程序包的其中一些類。如同眾所周知的 Class 類一樣, Java 使用這些類將我們所編寫的程序演示為對象。為了描述對象,我們需要知道它的組成。我們從哪裡開始呢?那就從這個類開始吧,因為它包含了我們的所有代碼。

Class<?> clazz = object.getClass();
注意到這裡的泛型聲明 Class<?>。泛型,簡單地說,就是通過確保給出的實例是某種指定的類型提供類型安全的操作。我們的方法 (describeInstance(Object)) 並未綁定到某個特定的類型,而是設計為與任何給定的對象共同工作。因此,使用無限制的通配符 <?>。

Class 類有很多方法。我們將重點介紹與我們有關的方法。在下面的代碼片段中列出了這些方法。

Constructor<?>[] constructors = clazz.getDeclaredConstructors();Field[] fields = clazz.getDeclaredFields();Method[] methods = clazz.getDeclaredMethods();
上面的 Class 方法返回了一組組成該對象構造函數、字段以及方法。

請注意,Class 類包含兩組 getter 方法:一組在其名稱中包含 declared 單詞,而另一組則不包含這個單詞。不同之處在於, getDeclaredMethods() 將返回屬於這個類的所有方法,而 getMethods() 只返回 public 方法。理解只返回在這個類中聲明的方法,這一點非常重要。繼承的方法是不會被檢索到的。

理解 ReflectionUtil 類沒有對 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.ProductSummary-----------------------------------------Constructors: 1Fields: 4Methods: 8Details-----------------------------------------Constructors:public ria.Product()Fields:private java.lang.String ria.Product.descriptionprivate long ria.Product.idprivate java.lang.String ria.Product.nameprivate double ria.Product.priceMethods: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)的方法,該方法返回給定實例的字段的值。

例如,我們的 Product 類。該類具有四個實例變量。檢索到的值取決於實例,因為不同的實例可能有不同的值。因此,必須向 Field 提供實例,才能返回如下所示的值:

field.get(object)
其中 field 是 Field 的一個實例,並且 object 是任何 Java 類的一個實例。

在我們開始草率地添加任何代碼之前,我們必須認識到這麼一個事實,那就是類的字段具有 private 訪問修改程序。如果我們按原樣調用 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 + ")");}}}
為了向您顯示這段代碼的效果,我來創建 of the java.awt.Rectangle 類的一個實例並使用describeInstance(Object) 方法打印其詳細信息。

Rectangle rectangle = new Rectangle(1, 2, 100, 200);ReflectionUtil.describeInstance(rectangle);
上面的這個代碼片段應該產生類似於以下內容的輸出。 請注意,某些輸出可能會由於過長而無法顯示被截斷。

Description for class: java.awt.RectangleSummary-----------------------------------------Constructors: 7Fields: 5Methods: 39Details-----------------------------------------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.xpublic int java.awt.Rectangle.ypublic int java.awt.Rectangle.widthpublic int java.awt.Rectangle.heightprivate static final long java.awt.Rectangle.serialVersionUIDMethods: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 = 1y = 2width = 100height = 200serialVersionUID = -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>)) 實例化擁有默認構造函數的任何類中的對象。也可以使用第二個方法。通過傳遞參數類型及其各自參數中的值,將通過匹配構造函數來實現實例化。例如,可以使用具有四個類型為 int 的參數的構造函數對 Rectangle 類進行實例化,使用的代碼如下所示:

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]);
invoke 方法來自 Method 類,需要兩個參數:將調用方法的實例以及作為對象數組的參數列表。請注意,方法doNothing() 沒有參數。盡管這樣,我們仍然還需要將參數指定為空的對象數組。

方法還具有一個返回類型;本例中為 void。可以將該返回類型(如果有)另存為 Object,某些內容類似於以下示例。

Object returnValue = method.invoke(object, new Object[0]);
在本例中,返回值為 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 方法。請再次注意,方法可以是 private,因此在調用它們之前最好設置它們的可訪問性。

 

應用程序中的反射
直到現在,我們僅創建了食用方法並且試驗了幾個簡單的示例。實際的編程需要的不只這些。想像我們需要搜索我們的對象並確定給定對象是否符合某些條件。第一個選項是編寫一個接口並在每個對象(如果該實例符合條件,則對象返回 true,否則返回 false)中實現它。不幸的是,該方法要求我們在我們擁有的每個類中執行一個方法。新的類不許實現該接口並為其抽象方法提供主要部分。或者,我們也可以使用反射檢索對象的字段並檢查它們的值是否符合條件。

讓我們首先創建另一個返回該對象字段的方法。請記住,沒有一種內置的方法可以返回所有字段,包括繼承的字段。因此,我們需要通過逐組提取它們來親自檢索它們,直到我們達到層次結構的頂部為止。可以向ReflectionUtil 類中添加該方法。

public static List <Field> getDeclaredFields(Class clazz) {List<Field> fields = new ArrayList<Field>();do {try {fields.addAll(Arrays.asList(clazz.getDeclaredFields()));} catch (Exception e) { }} while ((clazz = clazz.getSuperclass()) != null);return fields;}
現在,我們只需要讓它們的字符串值與給定的條件相匹配,如下面的代碼片段中所示。使用 String 方法valueOf(Object) 將字段的值轉換為字符串,而不返回 null 或拋出任何異常。請注意,這可能並不總是適合於復雜的數據類型。

public static boolean search(Object object, String criteria)throws IllegalArgumentException, IllegalAccessException {List <Field> fields = ReflectionUtil.getDeclaredFields(object.getClass());for (Field field : fields) {field.setAccessible(true);if (String.valueOf(field.get(object)).equalsIgnoreCase(criteria)) {return true;}}return false;}
讓我們創建一個名為 Address 的新類,並用該類進行試驗。該類的代碼如下所示。

package ria;public class Address {private String country;private String county;private String street;private String town;private String unit;//Getters and setters are omitted for shortness}
現在,讓我們創建 Book 和 Address 類的一個實例並應用我們的搜索方法。

Book book = new Book();book.setId(200);book.setName("Reflection in Action");book.setIsbn("123456789-X");book.setDescription("An article about reflection");Address address = new Address();address.setUnit("1");address.setStreet("Republic Street");address.setTown("Valletta");address.setCountry("Malta");System.out.println("Book match? " + search(book, "Valletta"));System.out.println("Address match? " + search(address, "Valletta"));
第一個匹配(針對 Book 實例的匹配)將返回 false,而地址實例將返回 true。可以針對任何對象應用此搜索方法,而無需添加或執行任何內容。

反射的缺點
直到現在,我們僅僅討論了反射如何好以及它如何使生活更輕松。不幸的是,任何事情都有代價。盡管反射功能非常強大並且提供了很大的靈活性,但是我們 不應該使用反射編寫任何內容。如果可能的話,在某些情況下您可能希望避免使用反射。因為反射會引入以下缺點:性能開銷、安全限制以及暴露隱藏的成員。

有時,通過訪問修改程序保存邏輯。下面的代碼片段就是一個鮮明的例子:

public class Student {private String name;public Student(String name){this.name = name;}}
當初始化對象後,只能通過構造函數更改學生的姓名。使用反射,您可以將學生的姓名設置任何 String,甚至在初始化對象之後也可以。正如您所見到的一樣,這樣會打亂業務邏輯並且可能會使程序行為不可預測。

與大多數其他編譯器一樣,Java 編譯器嘗試盡可能多的優化代碼。對於反射這是不可能的,因為反射是在運行時解析類型,而編譯器是在編譯時工作。此外,必須在稍後的階段即運行時解析類型。

結束語
反射可用於在不同對象中實現相同的邏輯(如搜索), 而不需要為每個新類型都創建新代碼。這樣也有利於對邏輯進行集中管理。遺憾的是,反射也存在缺點,它有時會增加代碼的復雜性。性能是對反射的另一個負面影響,因為無法在此類代碼上執行優化。

 
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved