首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》5.2 使用方法句柄

关灯直达底部

如果你不熟悉Java的反射API(ClassMethodField和它们的朋友),可以大致浏览一下(甚至跳过)这一节的内容。可如果你的代码库中有很多反射代码,那么你一定要认真读一读,因为它介绍了Java 7中取得相同效果的新办法,而且所用的代码更简洁。

Java 7为间接调用方法引入了新的API。其中的关键是java.lang.invoke包,即方法句柄。你可以把它看做反射的现代化方式,但它不像反射API那样有时会显得冗长、繁重和粗糙。

取代反射代码

反射中有很多套路化的代码。如果你写过一些反射代码,就不会忘记必须一次又一次地用Class指向内省方法的参数类型,并把该方法的参数都封装成Object,还要捕捉各种讨厌的异常以防出错,而且反射代码看起来也很不直观。

通过将反射代码转移到方法句柄,可以去掉套路化的代码,提高代码的可读性,这是大势所趋。

方法句柄是将invokedynamic(详情参见5.5节)引入JVM项目中的一部分。但其作用不仅限于invokedynamic的应用案例,在框架和常规用户代码中也有用武之地。接下来我们会先介绍方法句柄的基本技术;之后会给出一个例子与现有的各种方式进行比较,并总结出其中的差异。

5.2.1 MethodHandle

什么是MethodHandle?它是对可直接执行的方法(或域、构造方法等)的类型化引用,这是标准答案。还有一种说法:方法句柄是一个有能力安全调用方法的对象。

下面我们要获取一个带有两个参数的方法(但我们可能连这个方法的名字都不知道)的方法句柄,之后调用对象obj上的句柄,传入参数arg0arg1

MethodHandle mh = getTwoArgMH;MyType ret;try {  ret = mh.invokeExact(obj, arg0, arg1);} catch (Throwable e) {    e.printStackTrace;}  

这种能力有些像反射,还有些像4.4节介绍的Callable接口。实际上,Callable是对方法调用能力建模的早期尝试。但它只适用于不带参数的方法。为了满足现实情况中不同参数组合和调用的可能,我们需要编写带有特定参数组合的其他接口。

Java 6中有很多这种代码,接口四处蔓延,让开发人员万分苦恼(比如耗光保存类信息的PermGen内存——见第6章)。相比较而言,方法句柄则适用于任何方法签名,不需要产生那么多小类。这要归功于新引入的MethodType类。

5.2.2 MethodType

MethodType是表示方法签名类型的不可变对象。每个方法句柄都有一个MethodType实例,用来指明方法的返回类型和参数类型。但它没有方法的名字和“接收者类型”,即调用的实例方法的类型。

MethodType类中的工厂方法可以得到MethodType实例。这里有几个例子:

MethodType mtToString = MethodType.methodType(String.class);MethodType mtSetter = MethodType.methodType(void.class, Object.class);MethodType mtStringComparator = MethodType.methodType(int.class,String.class, String.class);  

这些MethodType实例分别表示toString,setter方法(Object类的成员)和Comparator<String>定义的compareTo方法的类型签名。MethodType实例一般都遵循相同的模式,第一个传入的参数是方法的返回类型,随后的参数是方法参数的类型(跟Class对象一样),如下所示:

MethodType.methodType(RetType.class, Arg0Type.class, Arg1Type.class, ...);  

你看,现在可以用普通对象来表示不同的方法签名了,不需要再逐一为它们定义新类型。这也在最大程度上保证了类型安全性,而且办法还很简单。如果你想知道某个方法句柄能否用特定的参数集调用,可以检查该句柄的MethodType

现在你应该明白MethodType是如何解决接口泛滥的问题了,接下来就去看看怎么得到指向类中方法的方法句柄吧。

5.2.3 查找方法句柄

下面的代码展示了如何得到指向当前类中toString方法的方法句柄。注意,mtToStringtoString的签名完全一致,返回类型为String,没有参数。也就是说相应的MethodType实例是MethodType.methodType(String.class)

代码清单5-2 查找方法句柄

public MethodHandle getToStringMH {  MethodHandle mh;  MethodType mt = MethodType.methodType(String.class);  //获取上下文    MethodHandles.Lookup lk = MethodHandles.lookup;  try { //从上下文中查找方法句柄    mh = lk.findVirtual(getClass, /"toString/", mt);  } catch (NoSuchMethodException | IllegalAccessException mhx) {    throw (AssertionError)new AssertionError.initCause(mhx);  }    return mh;} 

取得新的方法句柄要用lookup对象,比如代码清单5-2中的lk。这个对象可以提供其所在环境中任何可见方法的方法句柄。

要从lookup对象中得到方法句柄,你需要给出持有所需方法的类、方法的名称,以及跟你所需的方法签名相匹配的MethodType

注意 在查找上下文(lookup context)中可以得到任何类型(包括系统类型)中的方法句柄。当然,如果要从没有关联的类中取得句柄,查找上下文中只能看到或取得public方法的句柄。就是说方法句柄总是在安全管理之下安全使用——没有反射中setAccessible那种破解方法。

现在你已经拿到了方法句柄,接下来自然是执行它。方法句柄API为此提供了两个方法:invokeExactinvokeinvokeExact方法要求其参数类型与底层方法所期望的参数类型完全匹配。invoke方法会在参数类型不太正确时做些修改,以使其与底层方法参数相匹配(比如在需要时进行装箱或拆箱)。

接下来我们会给出一个长一点儿的例子,说明如何使用方法句柄取代过去的技术,比如反射和小型代理类。

5.2.4 示例:反射、代理与方法句柄

如果你曾经处理过满是反射的代码库,就会深知反射代码所带来的痛苦了。在本节中,我们要向你证明方法句柄可以取代很多套路化的反射代码,会让你的编码生涯更轻松。

代码清单5-3是改编自前面章节的例子。ThreadPoolManager负责将新任务分配给线程池,和代码清单4-15稍有不同。它还能取消正在运行的任务,但是个私有方法。

为了阐明方法句柄和其他技术之间的差别,我们给出了从外部访问类的私有方法cancel的三种办法:makeReflectivemakeProxymakeMh。我们还展示了两种Java 6技术:反射和代理类。并且和基于MethodHandle的方式进行了比较。我们用到了一个读取队列的任务QueueReaderTask(实现了Runnable接口)。你可以在本章源码中找到QueueReaderTask实现。

代码清单5-3 三种访问方式

public class ThreadPoolManager {  private final ScheduledExecutorService stpe =Executors.newScheduledThreadPool(2);  private final BlockingQueue<WorkUnit<String>> lbq;  public ThreadPoolManager(BlockingQueue<WorkUnit<String>> lbq_) {    lbq = lbq_;    }  public ScheduledFuture<?> run(QueueReaderTask msgReader) {    msgReader.setQueue(lbq);    return stpe.scheduleAtFixedRate(msgReader, 10, 10,TimeUnit.MILLISECONDS);    }    //要访问的私有方法    private void cancel(final ScheduledFuture<?> hndl) {     stpe.schedule(new Runnable {       public void run { hndl.cancel(true); }      }, 10, TimeUnit.MILLISECONDS);    }    public Method makeReflective {      Method meth = null;      try {        Class<?> argTypes = new Class { ScheduledFuture.class };        meth = ThreadPoolManager.class.getDeclaredMethod(/"cancel/",     argTypes);           meth.setAccessible(true);//要求访问私有方法        }  catch (IllegalArgumentException | NoSuchMethodException     | SecurityException e) {           e.printStackTrace;        }        return meth;    }    public static class CancelProxy {      private CancelProxy { }      public void invoke(ThreadPoolManager mae_, ScheduledFuture<?> hndl_) {          mae_.cancel(hndl_);        }    }    public CancelProxy makeProxy {      return new CancelProxy;    }    public MethodHandle makeMh {      MethodHandle mh;      //创建MethodType        MethodType desc = MethodType.methodType(void.class,    ScheduledFuture.class);      try {       //查找MethodHandle           mh = MethodHandles.lookup    .findVirtual(ThreadPoolManager.class, /"cancel/", desc);            } catch (NoSuchMethodException | IllegalAccessException e) {            throw (AssertionError)new AssertionError.initCause(e);        }        return mh;    }}  

这个类提供了三个访问私有方法cancel的方法。实际上,一般实现时只会用一个,我们是为了讨论它们之间的差别才全都列了出来。

下面是使用这些方法的例子。

代码清单5-4 使用这些访问方法

private void cancelUsingReflection(ScheduledFuture<?> hndl) {  Method meth = manager.makeReflective;  try {      System.out.println(/"With Reflection/");      meth.invoke(hndl);  } catch (IllegalAccessException | IllegalArgumentException  | InvocationTargetException e) {      e.printStackTrace;    }}private void cancelUsingProxy(ScheduledFuture<?> hndl) {  CancelProxy proxy = manager.makeProxy;  System.out.println(/"With Proxy/");   //通过代理调用是静态类型的    proxy.invoke(manager, hndl);}private void cancelUsingMH(ScheduledFuture&lt;?&gt; hndl) {  MethodHandle mh = manager.makeMh;  try {      System.out.println(/"With Method Handle/");       //方法签名必须完全一致         mh.invokeExact(manager, hndl);      } catch (Throwable e) { //必须捕捉Throwable        e.printStackTrace;    }}BlockingQueue<WorkUnit<String>> lbq = new LinkedBlockingQueue<>;manager = new ThreadPoolManager(lbq);final QueueReaderTask msgReader = new QueueReaderTask(100) {  @Override    public void doAction(String msg_) {      if (msg_ != null) System.out.println(/"Msg recvd: /"+ msg_);    }}; //然后用hndl取消任务hndl = manager.run(msgReader); 

这几个cancelUsing方法都有一个ScheduledFuture参数,所以你可以用前面的代码试验不同的取消方法。实际上,作为API的使用者,你可以不用去管这是如何实现的。

在下一节中,我们会告诉你API或框架开发人员用方法句柄取代其他方式的原因。

5.2.5 为什么选择MethodHandle

在上一节中我们看了一个把方法句柄用在Java 6中使用反射和代理的地方的例子。这引出了一个问题:为什么要用方法句柄取代过去的处理方式?

从表5-1可以看出,反射最大的优势就是人们熟悉它。代理对于简单用例可能更容易理解,但我们认为方法句柄在这两方面做得都是最棒的。我们强烈推荐你使用方法句柄。

表5-1 Java的方法间接访问技术比较

特性反射代理方法句柄访问控制必须使用setAccesible。会被安全管理器禁止内部类可以访问受限方法在恰当的上下文中对所有方法都有完整的访问权限。和安全管理器没有冲突类型纪律(Type discipline)没有。不匹配就抛出异常静态的。过于严格。为了存储全部的代理类,可能需要很多PermGen在运行时是类型安全的。不占用PermGen性能跟其他的比算慢的跟其他方法调用一样快力求跟其他方法调用一样快

方法句柄还有一个特性,可以从静态上下文中确定当前类。如果你曾经编写过这样的日志代码(比如log4j):

Logger lgr = LoggerFactory.getLogger(MyClass.class);  

你应该知道这样的代码很脆弱。如果它被重构进超类或子类中,显式声明的类名就会有问题。然而在Java 7中,你可以这样写:

Logger lgr = LoggerFactory.getLogger(MethodHandles.lookup.lookupClass);  

在这行代码中,可以把lookupClass看成用在静态上下文中的getClass。这在处理日志框架之类的场合中特别有用,因为通常每个用例都有自己的logger。

带着新掌握的方法句柄技术,我们去检查一下类文件的底层细节和使其变得有意义的工具。