Java 8的Lambda表示式的陰暗面

banq發表於2014-04-03


這是來自Tai Weiss的一篇博文,引發了不少討論,大意如下:Java 8最大的特色是Lambda表示式,Lambda曾經是函式語言代表Scala和Clojure的顯著特徵,如今Java也加入了。

Java 8第二個最大特色是Nashorn,這是一個新的JVM Javascript引擎,能夠將Java和JS引擎V8及其應用Node.js混合程式設計。

但是這些新特色也有其陰性面。

Java平臺是由兩個主要元件組成,JRE和JDK,這兩個元件應該是解耦的,這樣能讓人們編寫自己的JVM語言,Scala正是這樣的代表。

JVM可以執行任何語言編寫的程式碼,只要它們能編譯成位元組碼,位元組碼自身是充分OO的,被設計成接近於Java語言,這意味著Java被編譯成的位元組碼非常容易被重新組裝。

但是如果不是Java語言,差距將越來越大,Scala原始碼和被編譯成的位元組碼之間巨大差距是一個證明,編譯器加入了大量合成類 方法和變數,以便讓JVM按照語言自身特定語法和流程控制執行。

當你使用真正動態語言Javascript時,這種差距變得巨大。

如果這只是一個理論問題而不影響實際工作,那也就罷了,可惜不是,當我們加入新的元素進入Java,你的程式碼和執行情況之間有了距離,這意味著你編寫程式碼和你除錯程式碼是兩回事

我們首先看看Java 6/7中的一個傳統方法案例:

// simple check against empty strings
public static int check(String s) {
    if (s.equals("")) {
        throw new IllegalArgumentException();
    }
    return s.length();
}
 
//map names to lengths
 
List lengths = new ArrayList();
 
for (String name : Arrays.asList(args)) {
    lengths.add(check(name));
}
<p class="indent">

如果一個空的字串傳入,這段程式碼將丟擲錯誤,堆疊跟蹤如下:

at LmbdaMain.check(LmbdaMain.java:19)
at LmbdaMain.main(LmbdaMain.java:34)
<p class="indent">

這裡我們看到出錯堆疊和我們程式碼之間是1:1的對應,這使得我們除錯變得直接。

那麼現在看看Scala和Java 8:
在Scala中只要使用Lambda表示式map來實現這段程式碼,如下:

val lengths = names.map(name => check(name.length))
<p class="indent">


如果一個錯誤丟擲,呼叫棧將變得很長,如下:

at Main$.check(Main.scala:6)
at Main$$anonfun$1.apply(Main.scala:12)
at Main$$anonfun$1.apply(Main.scala:12)
at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
at scala.collection.immutable.List.foreach(List.scala:318)
at scala.collection.TraversableLike$class.map(TraversableLike.scala:244)
at scala.collection.AbstractTraversable.map(Traversable.scala:105)
at Main$delayedInit$body.apply(Main.scala:12)
at scala.Function0$class.apply$mcV$sp(Function0.scala:40)
at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
at scala.App$$anonfun$main$1.apply(App.scala:71)
at scala.App$$anonfun$main$1.apply(App.scala:71)
at scala.collection.immutable.List.foreach(List.scala:318)
at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:32)
at scala.App$class.main(App.scala:71)
at Main$.main(Main.scala:1)
at Main.main(Main.scala)
<p class="indent">

記住這個例子還是很簡單。現實世界中巢狀lambda和複雜的結構,你會看更長的合成的呼叫堆疊,從中你還需要了解程式發生了什麼事。

再看看Java 8:

Stream lengths = names.stream().map(name -> check(name));
 
at LmbdaMain.check(LmbdaMain.java:19)
at LmbdaMain.lambda$0(LmbdaMain.java:37)
at LmbdaMain$$Lambda$1/821270929.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.LongPipeline.reduce(LongPipeline.java:438)
at java.util.stream.LongPipeline.sum(LongPipeline.java:396)
at java.util.stream.ReferencePipeline.count(ReferencePipeline.java:526)
at LmbdaMain.main(LmbdaMain.java:39)
<p class="indent">

這非常類似Scala,出錯棧資訊太長,我們為程式碼的精簡付出力代價,更精確的程式碼意味著更復雜的除錯。

Javac必須支援Lambda函式,但是JVM還必須無視它們,這樣才能保證JVM操作在一個低階別層次,而無需引入任何新的規範元素。

當你明白這個設計決定的秘密,意味著Java程式設計師需要付出區分呼叫堆疊資訊的成本。這種問題在java8 + javascript上表現得更加突出。(好長的堆疊資訊可見原文,點按標題進入)

本站Java8 Lambda相關文章:點按這裡

[該貼被banq於2014-04-03 14:39修改過]

相關文章