[翻译]Reflection in Action

Posted on 2008-02-23 11:37 Norvid 阅读(946) 评论(0)  编辑  收藏 所属分类: 翻译
原文: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)的详情。或许你已经预料到了,这个类会包含一些实用的方法,它们将完成这个程序所需要的反射功能。目前,这个类只会包含一个方法,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(12100200);
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
-----------------------------------------
= 1
= 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 = { 12100200 };
Class[] paramClazzes 
= { int.classint.classint.classint.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.




累死了!
未完待续。。。。

 


只有注册用户登录后才能发表评论。


网站导航:
 

posts - 0, comments - 9, trackbacks - 0, articles - 13

Copyright © Norvid