strawren 发表于 2013-1-27 05:00:28

Java 的反射机制详解

Java 的反射机制是使其具有动态特性的非常关键的一种机制,也是在JavaBean 中广泛应用的一种特性。
运用JavaBean的最常见的问题是:根据指定的类名,类字段名和所对应的数据,得到该类的实例.
Reflection 是 Java 程序开发语言的特征之一,它允许运行中的 Java程序对自身进行检查,或者说"自审",并能直接操作程序的内部属性。例如,使用它能获得 Java 类中各成员的名称并显示出来。
Java 的这一能力在实际应用中也许用得不是很多,但是在其它的程序设计语言中根本就不存在这一特性。例如,Pascal、C 或者 C++中就没有办法在程序中获得函数定义相关的信息。
JavaBean 是 reflection 的实际应用之一,它能让一些工具可视化的操作软件组件。这些工具通过 reflection 动态的载入并取得Java 组件(类) 的属性。
1. 一个简单的例子
考虑下面这个简单的例子,让我们看看 reflection 是如何工作的。
import java.lang.reflect.*;
public class DumpMethods {
   public staticvoid main(String args[]) {
       try {
           Class c =Class.forName(args);
           Method m[] =c.getDeclaredMethods();
           for (int i = 0;i<m.length;i++){
    System.out.println(m.toString());
    }
       } catch (Throwable e) {
          System.err.println(e);
       }
   }
}
按如下语句执行:
java DumpMethods java.util.Stack
它的结果输出为:
public java.lang.Object java.util.Stack.push(java.lang.Object)
public synchronized java.lang.Object java.util.Stack.pop()
public synchronized java.lang.Object java.util.Stack.peek()
public boolean java.util.Stack.empty()
public synchronized int java.util.Stack.search(java.lang.Object)
这样就列出了java.util.Stack 类的各方法名以及它们的限制符和返回类型。
这个程序使用 Class.forName 载入指定的类,然后调用 getDeclaredMethods来获取这个类中定义了的方法列表。java.lang.reflect.Methods 是用来描述某个类中单个方法的一个类。
2.开始使用 Reflection
用于 reflection 的类,如 Method,可以在 java.lang.relfect包中找到。使用这些类的时候必须要遵循三个步骤:第一步是获得你想操作的类的 java.lang.Class 对象。在运行中的 Java 程序中,用java.lang.Class 类来描述类和接口等。
下面就是获得一个 Class 对象的方法之一:
Class c = Class.forName("java.lang.String");
这条语句得到一个 String 类的类对象。还有另一种方法,如下面的语句:
Class c = int.class;
或者
Class c = Integer.TYPE;
它们可获得基本类型的类信息。其中后一种方法中访问的是基本类型的封装类 (如 Integer) 中预先定义好的 TYPE 字段。
第二步是调用诸如 getDeclaredMethods 的方法,以取得该类中定义的所有方法的列表。
一旦取得这个信息,就可以进行第三步了——使用 reflection API 来操作这些信息,如下面这段代码:
Class c = Class.forName("java.lang.String");
Method m[] = c.getDeclaredMethods();
System.out.println(m.toString());
它将以文本方式打印出 String 中定义的第一个方法的原型。
在下面的例子中,这三个步骤将为使用 reflection 处理特殊应用程序提供例证。
模拟 instanceof 操作符
得到类信息之后,通常下一个步骤就是解决关于 Class 对象的一些基本的问题。例如,Class.isInstance 方法可以用于模拟instanceof 操作符:
class A {
}
public class instance1 {
   public static void main(String args[]){
       try {
           Class cls = Class.forName("A");
          boolean b1 = cls.isInstance(new Integer(37));
          System.out.println(b1);
           boolean b2 = cls.isInstance(newA());
           System.out.println(b2);
       } catch (Throwable e){
           System.err.println(e);
       }
   }
}
在这个例子中创建了一个 A 类的 Class 对象,然后检查一些对象是否是 A 的实例。Integer(37) 不是,但 new A() 是。
3.找出类的方法
找出一个类中定义了些什么方法,这是一个非常有价值也非常基础的 reflection 用法。下面的代码就实现了这一用法:
import java.lang.reflect.*;
public class Method1 {
   private int f1(Object p, int x) throwsNullPointerException {
       if (p == null)
           throw newNullPointerException();
       return x;
   }
   public static void main(String args[]) {
       try {
          Class cls = Class.forName("Method1″);
           Method methlist[] =cls.getDeclaredMethods();
           for (int i = 0;i<methlist.length;i++){
      Method m = methlist;
              System.out.println("name = " + m.getName());
              System.out.println("decl class = " + m.getDeclaringClass());
              Class pvec[] = m.getParameterTypes();
               for (int j = 0;j<pvec.length;j++){                        
     System.out.println("param #" + j + " " + pvec);
     }
               Class evec[] = m.getExceptionTypes();
               for(int j = 0; j <evec.length;i++){
       System.out.println("exc #" + j +":" + evec);
       System.out.println("return type = " +m.getReturnType());
      };
              System.out.println("—–");
      }
           }
       } catch(Throwable e) {
           System.err.println(e);
       }
  }
}
这个程序首先取得 method1 类的描述,然后调用 getDeclaredMethods 来获取一系列的 Method对象,它们分别描述了定义在类中的每一个方法,包括 public 方法、protected 方法、package 方法和 private方法等。如果你在程序中使用 getMethods 来代替 getDeclaredMethods,你还能获得继承来的各个方法的信息。
取得了 Method对象列表之后,要显示这些方法的参数类型、异常类型和返回值类型等就不难了。这些类型是基本类型还是类类型,都可以由描述类的对象按顺序给出。
输出的结果如下:
name = f1
decl class = class Method1
param #0 class java.lang.Object
param #1 int
exc #0 class java.lang.NullPointerException
return type = int
—–
name = main
decl class = class Method1
param #0 class [Ljava.lang.String;
return type = void
—–
4.获取构造器信息
获取类构造器的用法与上述获取方法的用法类似,如:
import java.lang.reflect.*;
public class Constructor1 {
   public Constructor1() {
   }
   protected constructor1(int i, double d) {
   }
   public static void main(String args[]) {
       try {
          Class cls = Class.forName("Constructor1″);
           Constructor ctorlist[]= cls.getDeclaredConstructors();
           for (int i = 0;i<ctorlist.length;i++){
      Constructor ct =ctorlist;
               System.out.println("name = " +ct.getName());
               System.out.println("decl class = " +ct.getDeclaringClass());
               Class pvec[] =ct.getParameterTypes();
      for (int j = 0;j.pvec.length;j++){
     System.out.println("param #" + j + " " +pvec);
    }
      Class evec[] =ct.getExceptionTypes();
               for (int j = 0;j<evec.lenght;j++){                      
     System.out.println("exc #"+ j + " " + evec);
      }
              System.out.println("—–");
           }
       } catch (Throwable e){
           System.err.println(e);
       }
   }
}
这个例子中没能获得返回类型的相关信息,那是因为构造器没有返回类型。
这个程序运行的结果是:
name = Constructor1
decl class = class Constructor1
—–
name = Constructor1
decl class = class Constructor1
param #0 int
param #1 double
—–
5.获取类的字段(域)
找出一个类中定义了哪些数据字段也是可能的,下面的代码就在干这个事情:
import java.lang.reflect.*;
public class Field1 {
   private double d;
   public static final int i= 37;
   String s = "testing";
   public static void main(String args[]) {
       try {
          Class cls = Class.forName("Field1");
           Field fieldlist[] =cls.getDeclaredFields();
           for (int i = 0;i<fieldlist.length;i++){
      Field fld = fieldlist;
              System.out.println("name = " + fld.getName());
              System.out.println("decl class = " + fld.getDeclaringClass());
              System.out.println("type = " + fld.getType());
               int mod =fld.getModifiers();
               System.out.println("modifiers = " +Modifier.toString(mod));
              System.out.println("—–");
           }
       } catch (Throwable e){
           System.err.println(e);
       }
   }
}
这个例子和前面那个例子非常相似。例中使用了一个新东西 Modifier,它也是一个 reflection 类,用来描述字段成员的修饰语,如"privateint"。这些修饰语自身由整数描述,而且使用 Modifier.toString 来返回以"官方"顺序排列的字符串描述(如"static"在"final"之前)。这个程序的输出是:
name = d
decl class = class Field1
type = double
modifiers = private
—–
name = i
decl class = class Field1
type = int
modifiers = public static final
—–
name = s
decl class = class field1
type = class java.lang.String
modifiers =
—–
和获取方法的情况一下,获取字段的时候也可以只取得在当前类中申明了的字段信息 (getDeclaredFields),或者也可以取得父类中定义的字段(getFields) 。
6.根据方法的名称来执行方法
文本到这里,所举的例子无一例外都与如何获取类的信息有关。我们也可以用 reflection来做一些其它的事情,比如执行一个指定了名称的方法。下面的示例演示了这一操作:
import java.lang.reflect.*;
public class Method2 {
   public intadd(int a, int b) {
       return a + b;
   }
   public static voidmain(String args[]) {
       try {
           Class cls =Class.forName("Method2");
           Class partypes[] = newClass;
           partypes = Integer.TYPE;
           partypes =Integer.TYPE;
           Method meth = cls.getMethod("add",partypes);
           method2 methobj = new method2();
           Objectarglist[] = new Object;
           arglist = newInteger(37);
           arglist = new Integer(47);
           Objectretobj = meth.invoke(methobj, arglist);
           Integer retval = (Integer)retobj;
           System.out.println(retval.intvalue());
       } catch(Throwable e) {
           System.err.println(e);
       }
  }
}
假如一个程序在执行的某处的时候才知道需要执行某个方法,这个方法的名称是在程序的运行过程中指定的 (例如,JavaBean开发环境中就会做这样的事),那么上面的程序演示了如何做到。
上例中,getMethod 用于查找一个具有两个整型参数且名为 add 的方法。找到该方法并创建了相应的 Method对象之后,在正确的对象实例中执行它。执行该方法的时候,需要提供一个参数列表,这在上例中是分别包装了整数 37 和 47 的两个 Integer对象。执行方法的返回的同样是一个 Integer 对象,它封装了返回值 84。
7.创建新的对象
对于构造器,则不能像执行方法那样进行,因为执行一个构造器就意味着创建了一个新的对象(准确的说,创建一个对象的过程包括分配内存和构造对象)。所以,与上例最相似的例子如下:
import java.lang.reflect.*;
public class Constructor2 {
   public Constructor2() {
   }
   public Constructor2(int a, int b) {
       System.out.println("a = " +a + " b = " + b);
   }
   public static void main(String args[]) {
       try {
          Class cls = Class.forName("Constructor2");
           Class partypes[] = newClass;
           partypes = Integer.TYPE;
           partypes =Integer.TYPE;
           Constructor ct =cls.getConstructor(partypes);
           Object arglist[] = newObject;
           arglist = new Integer(37);
           arglist= new Integer(47);
           Object retobj =ct.newInstance(arglist);
       } catch (Throwable e) {
          System.err.println(e);
       }
   }
}
根据指定的参数类型找到相应的构造函数并执行它,以创建一个新的对象实例。使用这种方法可以在程序运行时动态地创建对象,而不是在编译的时候创建对象,这一点非常有价值。
8.改变字段(域)的值
reflection 的还有一个用处就是改变对象数据字段的值。reflection可以从正在运行的程序中根据名称找到对象的字段并改变它,下面的例子可以说明这一点:
import java.lang.reflect.*;
public class Field2 {
   public double d;
   public static void main(String args[]) {
       try {
          Class cls = Class.forName("Field2");
           Field fld =cls.getField("d");
           field2 f2obj = new field2();
          System.out.println("d = " + f2obj.d);
           fld.setDouble(f2obj,12.34);
           System.out.println("d = " + f2obj.d);
       } catch(Throwable e) {
           System.err.println(e);
       }
  }
}
这个例子中,字段 d 的值被变为了 12.34。
9.使用数组
本文介绍的 reflection 的最后一种用法是创建的操作数组。数组在 Java 语言中是一种特殊的类类型,一个数组的引用可以赋给 Object引用。观察下面的例子看看数组是怎么工作的:
import java.lang.reflect.*;
public class Array1 {
   public static void main(String args[]){
       try {
           Class cls =Class.forName("java.lang.String");
           Object arr =Array.newInstance(cls, 10);
           Array.set(arr, 5, "this is atest");
           String s = (String) Array.get(arr, 5);
          System.out.println(s);
       } catch (Throwable e) {
          System.err.println(e);
       }
   }
}
例中创建了 10 个单位长度的 String 数组,为第 5 个位置的字符串赋了值,最后将这个字符串从数组中取得并打印了出来。
下面这段代码提供了一个更复杂的例子:
import java.lang.reflect.*;
public class Array2 {
   public static void main(String args[]){
       int dims[] = new int[]{5, 10, 15};
       Object arr =Array.newInstance(Integer.TYPE, dims);
       Object arrobj = Array.get(arr,3);
       Class cls = arrobj.getClass().getComponentType();
      System.out.println(cls);
       arrobj = Array.get(arrobj, 5);
      Array.setInt(arrobj, 10, 37);
       int arrcast[][][] = (int[][][])arr;
       System.out.println(arrcast);
   }
}
例中创建了一个 5x 10 x 15 的整型数组,并为处于 的元素赋了值为 37。注意,多维数组实际上就是数组的数组,例如,第一个 Array.get之后,arrobj 是一个 10 x 15 的数组。进而取得其中的一个元素,即长度为 15 的数组,并使用 Array.setInt 为它的第 10个元素赋值。
注意创建数组时的类型是动态的,在编译时并不知道其类型。

Visitor模式的常用之处在于,它将对象集合的结构和对集合所执行的操作分离开来。例如,它可以将一个编译器中的分析逻辑和代码生成逻辑分离开来。有了这样的分离,想使用不同的代码生成器就会很容易。更大的好处还有,其它一些公用程序,如lint,可以在使用分析逻辑的同时免受代码生成逻辑之累。不幸的是,向集合中增加新的对象往往需要修改已经写好的Visitor类。本文提出了一种在Java中实现Visitor模式的更灵活的方法:使用Reflection(反射)。


集合(Collection)普遍应用于面向对象编程中,但它也经常引发一些和代码有关的疑问。例如,"如果一个集合存在不同的对象,该如何对它执行操作?"

一种方法是,对集合中的每个元素进行迭代,然后基于所在的类,对每个元素分别执行对应的操作。这会很难办,特别是,如果你不知道集合中有什么类型的对象。例如,假设想打印集合中的元素,你可以写出如下的一个方法(method):

public void messyPrintCollection(Collection collection) {
   Iterator iterator = collection.iterator()
   while (iterator.hasNext())
      System.out.println(iterator.next().toString())
}

这看起来够简单的了。它只不过调用了Object.toString()方法,然后打印出对象,对吗?但如果有一组哈希表怎么办?事情就会开始变得复杂起来。你必须检查从集合中返回的对象的类型:

public void messyPrintCollection(Collection collection) {
   Iterator iterator = collection.iterator()
   while (iterator.hasNext()) {
      Object o = iterator.next();
      if (o instanceof Collection)
         messyPrintCollection((Collection)o);
      else
         System.out.println(o.toString());
   }
}

不错,现在已经解决了嵌套集合的问题,但它需要对象返回String,如果有其它不返回String的对象存在怎么办?如果想在String对象前后添加引号以及在Float后添加f又该怎么办?代码还是越来越复杂:

public void messyPrintCollection(Collection collection) {
   Iterator iterator = collection.iterator()
   while (iterator.hasNext()) {
      Object o = iterator.next();
      if (o instanceof Collection)
         messyPrintCollection((Collection)o);
      else if (o instanceof String)
         System.out.println("""+o.toString()+""");
      else if (o instanceof Float)
         System.out.println(o.toString()+"f");
      else
         System.out.println(o.toString());
   }
}

可以看到,事情的复杂度会急剧增长。你当然不想让一段代码到处充斥着if-else语句!那怎么避免呢?Visitor模式可以帮你。

要实现Visitor模式,得为访问者建立一个Visitor接口,还要为被访问的集合建立一个Visitable接口。然后,让具体类实现Visitor和Visitable接口。这两个接口如下所示:

public interface Visitor
{
   public void visitCollection(Collection collection);
   public void visitString(String string);
   public void visitFloat(Float float);
}

public interface Visitable
{
   public void accept(Visitor visitor);
}

对于具体的String,可能是这样:

public class VisitableString implements Visitable
{
   private String value;
   public VisitableString(String string) {
      value = string;
   }
   public void accept(Visitor visitor) {
      visitor.visitString(this);
   }
}

在accept方法中,对this类型调用正确的visitor方法:

visitor.visitString(this)

这样,就可以如下实现具体的Visitor:

public class PrintVisitor implements Visitor
{
   public void visitCollection(Collection collection) {
      Iterator iterator = collection.iterator()
      while (iterator.hasNext()) {
      Object o = iterator.next();
      if (o instanceof Visitable)
         ((Visitable)o).accept(this);
   }

   public void visitString(String string) {
      System.out.println("""+string+""");
   }

   public void visitFloat(Float float) {
      System.out.println(float.toString()+"f");
   }
}

实现VisitableFloat和VisitableCollection类的时候,它们也是各自调用合适的Visitor方法,所得到的效果和前面那个用了if-else的messyPrintCollection方法一样,但这里的手法更干净。在visitCollection()中,调用的是Visitable.accept(this),然后这个调用又返回去调用一个合适的Visitor方法。这被称做 "双分派";即,Visitor先调用了Visitable类中的方法,这个方法又回调到Visitor类中。

虽然通过实现visitor消除了if-else语句,却也增加了很多额外的代码。最初的String和Float对象都要用实现了Visitable接口的对象进行包装。这有点讨厌,但一般说来不是问题,因为你可以让经常被访问的集合只包含那些实现了Visitable接口的对象。

但似乎这还是额外的工作。更糟糕的是,当增加一个新的Visitable类型如VisitableInteger时,会发生什么呢?这是Visitor模式的一个重大缺陷。如果想增加一个新的Visitable对象,就必须修改Visitor接口,然后对每一个Visitor实现类中的相应的方法一一实现。你可以用一个带缺省空操作的Visitor抽象基类来代替接口。那就很象Java GUI中的Adapter类。那个方法的问题在于,它需要占用单继承;而你往往想保留单继承,让它用于其它什么东西,比如继承StringWriter。那个方法还有限制,它只能够成功访问Visitable对象。

幸运的是,Java可以让Visitor模式更灵活,使得你可以随心所欲地增加Visitable对象。怎么做?答案是,使用Reflection。比如,可以设计这样一个ReflectiveVisitor接口,它只需要一个方法:

public interface ReflectiveVisitor {
   public void visit(Object o);
}

就这样,很简单。至于Visitable,还是和前面一样,我过一会儿再说。现在先用Reflection来实现PrintVisitor:

public class PrintVisitor implements ReflectiveVisitor {
   public void visitCollection(Collection collection)
   { ... same as above ... }
   public void visitString(String string)
   { ... same as above ... }
   public void visitFloat(Float float)
   { ... same as above ... }

   public void default(Object o)
   {
      System.out.println(o.toString());
   }

   public void visit(Object o) {
      // Class.getName() returns package information as well.
      // This strips off the package information giving us
      // just the class name
      String methodName = o.getClass().getName();
      methodName = "visit"+
                   methodName.substring(methodName.lastIndexOf(".")+1);
      // Now we try to invoke the method visit
      try {
         // Get the method visitFoo(Foo foo)
         Method m = getClass().getMethod(methodName,
            new Class[] { o.getClass() });
         // Try to invoke visitFoo(Foo foo)
         m.invoke(this, new Object[] { o });
      } catch (NoSuchMethodException e) {
         // No method, so do the default implementation
         default(o);
      }
   }
}

现在不需要Visitable包装类。仅仅只是调用visit(),请求就会分发到正确的方法上。很不错的一点是,只要认为适合,visit()就可以分发。这并非必须使用reflection--它可以使用其它完全不同的机制。

新的PrintVisitor中,有针对Collection,String和Float而写的方法,但然后它又在catch语句中捕捉所有未处理的类型。你要扩展visit()方法,使得它也能够处理所有的父类。首先,得增加一个新方法,称为getMethod(Class c),它返回的是要调用的方法;为了找到这个相匹配的方法,先在类c的所有父类中寻找,然后在类c的所有接口中寻找。

protected Method getMethod(Class c) {
   Class newc = c;
   Method m = null;
   // Try the superclasses
   while (m == null && newc != Object.class) {
      String method = newc.getName();
      method = "visit" + method.substring(method.lastIndexOf(".") + 1);
      try {
         m = getClass().getMethod(method, new Class[] {newc});
      } catch (NoSuchMethodException e) {
         newc = newc.getSuperclass();
      }
   }
   // Try the interfaces.  If necessary, you
   // can sort them first to define "visitable" interface wins
   // in case an object implements more than one.
   if (newc == Object.class) {
      Class[] interfaces = c.getInterfaces();
      for (int i = 0; i < interfaces.length; i++) {
         String method = interfaces.getName();
         method = "visit" + method.substring(method.lastIndexOf(".") + 1);
         try {
            m = getClass().getMethod(method, new Class[] {interfaces});
         } catch (NoSuchMethodException e) {}
      }
   }
   if (m == null) {
      try {
         m = thisclass.getMethod("visitObject", new Class[] {Object.class});
      } catch (Exception e) {
          // Can"t happen
      }
   }
   return m;
}

看起来有些复杂,其实不然。实际上,它只是根据传进来的类名去寻找相应的方法而已。如果没找到,就在父类中找;还没找到,再到接口中找。最后,就拿visitObject()作为缺省。

注意,为了照顾那些熟悉传统Visitor模式的读者,我对方法的名称采用了传统的命名方式。但正如你们一些人所注意到的,把所有的方法命名为 "visit" 然后让参数类型作为区分会更高效。但这样做的话,你得把主visit(Object o)方法的名字改为dispatch(Object o)之类。否则,就没有一个缺省方法可用了,你就得在调用visit(Object o)时将类型转换为Object,以保证visit采用的是正确的调用方式。

现在可以修改visit()方法,以利用getMethod():

public void visit(Object object) {
   try {
     Method method = getMethod(getClass(), object.getClass());
     method.invoke(this, new Object[] {object});
   } catch (Exception e) { }
}

现在,visitor对象的功能强大多了。你可以传进任何对象,并且有某个方法处理它。另外一个好处是,还有一个缺省方法visitObject(Object o),它可以捕捉任何未知的对象。再多花点工夫,你还可以写出一个visitNull()方法。

我在上面对Visitable接口避而不谈自有原因。传统Visitor模式的另一个好处是,它允许Visitable对象来控制对对象结构的访问。例如,假设有一个实现了Visitable的TreeNode对象,你可以让一个accept()方法来遍历它的左右节点:

public void accept(Visitor visitor) {
   visitor.visitTreeNode(this);
   visitor.visitTreeNode(leftsubtree);
   visitor.visitTreeNode(rightsubtree);
}

这样,只用对Visitor类再进行一点修改,就可以进行Visitable控制访问:

public void visit(Object object) throws Exception
{
    Method method = getMethod(getClass(), object.getClass());
     method.invoke(this, new Object[] {object});
     if (object instanceof Visitable)
     {
          callAccept((Visitable) object);
     }
}
public void callAccept(Visitable visitable) {
   visitable.accept(this);
}

如果已经实现了一个Visitable对象结构,可以保留callAccept()方法并使用Visitable控制访问。如果想在visitor中访问结构,只需改写callAccept()方法,使之什么也不做。

想让数个不同的访问者对同一个对象集合进行访问时,Visitor模式可以发挥它的强大作用。假设已经有一个解释器,一个中缀写作器,一个后缀写作器,一个XML写作器和一个SQL写作器,它们都作用在同一个对象集合上。那么,也可以很容易地为相同的对象集合写出一个前缀写作器和一个SOAP写作器。另外,这些写作器可以正常地和它们所不知道的对象工作;当然,如果愿意,也可以让它们抛出异常。

结论
通过使用Java Reflection,你可以增强Visitor模式,使之具有操作对象结构的强大功能,并在增加新Visitable类型方面提供灵活性。希望你在以后的程序设计中能够应用这一模式。
页: [1]
查看完整版本: Java 的反射机制详解