23 February 2014

昨個兒花了半天嘗試將我的 專案 (side project) 轉到 Java8,我試的組合是:

  • Jdk8 RC2
  • Eclipse 4.3.1 配上 Beta plugin
  • Gradle 1.11
  • 專案程式行數 13500 (Spring framework based)

Gradle 之前舊版好像無法處理 jdk8 產生的 bytecodes,所以也一起升到最新版。第一印象是 Eclipse 現階段還不夠穩定,寫稍微複雜一點的 lambda 就一直噴 NullPointerException,不過勉強堪用。

Eclipse Clean Up

Eclipse 很早就有一個 Clean up 的功能,它可以替你 format 程式碼、變數加上 final 等等功能。新版的加上自動將 anonymous class 轉換為 lambda 的功能:

Clean up Dialog

這功能還不錯,我第一次轉整個專案,只有三個 Compile 錯誤、四個警告,其他一切正常。而錯誤的地方另人訝異,而正好都是 lambda 的罩門。看書時都覺得書中寫的頭頭是道,但一旦實戰就馬上撞牆,來看看我遇到了什麼問題吧:

Lambda Expression Parameter Scope

//原來的程式
void subscribe(Destination destination) {
  session.subscribe(destination, new Listener() {
    public void onData(Destination destination, Object data) {
       handlePublication(data);
    }
  });
}

//自動轉完 lambda 後,Compile error
void subscribe(Destination destination) {
  session.subscribe(destination, 
    (Destination destination, Object data) -> {
       handlePublication(data);
    });
}

上面的 lambda compile 失敗。why ? 因為 lambda 的參數 destination 和 外層 method 的 destination 相衝了,錯誤訊息是:

Lambda expression's parameter destination cannot redeclare another local variable defined in an enclosing scope.

Lambda 的 parameter scope 不在內層,而是跟外層的變數同級,所以命名要不同。雖然 compiler 會幫你抓這個問題,實務上不會有 shadow 的 bug。一開始很訝異 Java lambda
是這樣實作的,因為跟其他語言不大一樣。但也因為 lambda expression 和外層 scope 是同層級,所以 lambda 在捕捉 local 變數時,該變數不用再像以前一樣要宣告 final 了。

這個問題相信所有專案在做 lambda 化時最少會碰上一次,自動轉換很爽,但轉換後要小心做 code review。

參考資料 Accessing Local Variables of the Enclosing Scope

Lambda Expression does not support Generic Methods

//原來的程式,Spring 的 transaction template
worker.setTransactionTemplate(new TransactionOperations() {
  @Override
  public <T> T execute(final TransactionCallback<T> action) 
      throws TransactionException {
    return action.doInTransaction(null);
  }
});

//理論上可以轉成這樣,不過失敗。
worker.setTransactionTemplate(
  action -> action.doInTransaction(null));

//這樣也不行啦,syntax error
worker.setTransactionTemplate((TransactionCallback<T> action) 
  -> action.doInTransaction(null));

錯誤訊息是:

Illegal lambda expression: Method execute of type TransactionOperations is generic

錯誤的原因是 <T> T execute(...)Generic Methods。Lambda expression 不支援這種 method。這告訴我們如果要設計 interface 時 ,盡可能不要用 Generic Methods,即使你設計的函式庫現階段只打算給 Java7 以前使用。像是 TransactionOperations 是 Spring 裡很舊的 interface 了,如果他們有機會重來的話大概也不會這樣設計了。

Reference is Ambiguous

接下來的錯誤更妙了,在 Eclipse 下 ok,但是 javac compile 出錯,來看看 ExecutorService 的 submit method 吧:

ExecutorService executor = Executors.newCachedThreadPool();

//第一個例子,compiler 知道這是 Runnable 因為它沒有 return
executor.submit(() -> {
  doHardwork();
});

//第二個例子,compiler 知道這是 Callable<String> 因為 return String
executor.submit(() -> {
  doHardwork();
  return "I am Callable because I have return";
});

//第三個例子,這設計來跑 profiling 用的,所以永遠不會停
executor.submit(() -> {
  //Who am I?
  while(true) {
    doSomethingForever();
  }
});

來看看 javac 的錯誤訊息

reference to submit is ambiguous
  executor.submit(() -> {
          ^
  both method <T>submit(Callable<T>) in ExecutorService and method submit(Runnable) in ExecutorService match...

喔,submit() 同時可接受 RunnableCallable 兩種 interface,前兩個例子 compiler 很厲害,都可以自動判斷,但第三個例子就不行了,因為 while(true) 讓它不能寫 return (compiler 會報 unreachable statement 錯誤)。

奇妙的是 Eclipse 居然可以 compile 過,我可以確定 Eclipse 推導出的是 Callable,因為我的 doSomethingForever() 有 throw checked exception。Runnable 可不能吃 exception 啊。

那最後怎麼解決這問題呢?好在有另一個 method 只接受 Runnable: executor.execute(),改呼叫這個就好了。

這錯誤告訴我們什麼?當你設計的 method 打算接受 lambda 時,那就不要採用 overloading。你可以讓名稱類似,但在結尾做點變化。就像是Java8 裡新增的 CompletableFuture 那樣。不過我個人認為 CompletableFuture 做的太過火了,它的 method 數超過 50 個。誰會記得這麼多 API 呢?這是 anti-pattern。

實際使用 Stream + Lambda

最後一個項目就不談錯誤了,我實際找了一段程式碼來改寫成 lambda。這程式的用途是亂數產生大量的 socket client,連線到 server 做壓力測試。它有兩層 for-loop,也有 Callable,很適合拿來練功:

//需求:對每一個 topic,產生亂數量的 Client,
//並連線到 server,使用 Executor 執行以達到同時連線的目的。

String[] sampleTopics = { "foo", "bar", "baz" };
ExecutorService executor = Executors.newCachedThreadPool();

//原來的版本
public void original() {
  for (final String topic : sampleTopics) {
    int noOfClient = someRandomNumber();
    for (int i = 0; i < noOfClient; i++) {
      executor.submit(new Callable<Void>() {
        @Override
        public Void call() throws Exception {
          Client client = createClient(topic);
          client.connect();
          return null;
        }
      });
    }
  }
}

//lambda 版
public void lambda() {
  Arrays.stream(sampleTopics).flatMap(topic -> {
    int noOfClient = someRandomNumber();
    return Stream.<Callable<Void>>generate(() -> () -> {
      Client client = createClient(topic);
      client.connect();
      return null;
    }).limit(noOfClient);
  }).forEach(executor::submit);
}

如果你沒寫過其他 functional 語言,你可能要花一陣子才能消化 Stream 的程式。我也是一樣,因為是頭一次寫 lambda + stream,我花了半小時才搞定語法,最後才寫出有 Functional 醍醐味 的程式碼。但改寫的過程 Eclipse 一直噴 NullPointerException,真是煩,Eclipse Fundation 打算和 Java8 同時間推出正式版,也就是 3月18日,我很懷疑這個時程可以達到。

ok,回正題,改寫好之後,我第一個感想是:Scala! 我又回憶起當初寫 Scala 時出現的那些魔法符號,看看這一行:

Stream.generate(() -> () -> { ... });

可讀性很低啊,你不去查 generate() 的 API Doc 還看得懂那你真是很神。這正是標準的 寫的時候很爽,讀的時候很痛苦。也許以後常常使用 generate() 之後,內化成一種 idiom ,就不會有這種問題了,但肯定門檻又高了一些。

還有其他怪異的語法,像是 Stream.<Callable<Void>>generate() ,雖然 method 上加 generic type 在 Java5 就有了,但使用 lambda 之後會更常出現。這也要一段學習的時間。

最後還有一個 stream.flatMap(),如果你看的懂 flatMap 在玩什麼,恭喜你進入八奇... 喔!是 Monad 的領域。Monad 實在是,該怎麼說咧... 懂的人就懂,不懂的人還是不懂。這個題目太大,之後再提吧,我建議 Java 人先去讀大師 Mario Fusco 的投影片 Monadic Java,看完保證打通 Monad 任督二脈。

好了,這兩個版本,你覺得那個版本比較好呢?

題外話 GWT

我的賽專案其實還有一半程式碼是用 GWT 寫成,那又是另外一萬多行的程式,這些當然是轉不動的,雖然 Google 已經半放棄這個計畫了,不過其實還是有活動再進行的,預計今年就會看到 GWT 支援 lambda! GUI 的程式才是 lambda 最大用戶啊,很高興看到 GWT 繼續進化。接下來我要問 Android 的 lambda 在哪裡?嘿!Google 不要再混了!

小結

還沒有結束,我才玩半天的 lambda 而已,This is just the beginning!


回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>