讀古今文學網 > Java程序員修煉之道 > 5.2 使用方法句柄 >

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。

帶著新掌握的方法句柄技術,我們去檢查一下類文件的底層細節和使其變得有意義的工具。