反射实践

您是否考虑过这些问题: 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 下。类 ConstructorFieldMethod 就是属于这个程序包的其中一些类。如同众所周知的 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)

其中 fieldField 的一个实例,并且 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
}

现在,让我们创建 BookAddress 类的一个实例并应用我们的搜索方法。


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 编译器尝试尽可能多的优化代码。对于反射这是不可能的,因为反射是在运行时解析类型,而编译器是在编译时工作。此外,必须在稍后的阶段即运行时解析类型。

结束语

反射可用于在不同对象中实现相同的逻辑(如搜索), 而不需要为每个新类型都创建新代码。这样也有利于对逻辑进行集中管理。遗憾的是,反射也存在缺点,它有时会增加代码的复杂性。性能是对反射的另一个负面影响,因为无法在此类代码上执行优化。

参考资料

Albert Attard 在当地的 ICT 大学教授 Java 等各种编程语言,包括入门级和中级。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值