程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 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.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) 的方法,該方法返回給定實例的字段的值。

例如,我們的 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.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>)) 實例化擁有默認構造函數的任何類中的對象。 也可以使用第二個方法。通過傳遞參數類型及其各自參數中的值,將通過匹配構造函數來實現實例化。例 如,可以使用具有四個類型為 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