Java 8的Lambda表达式的阴暗面

14-04-03 banq
         

这是来自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>

如果一个空的字符串传入,这段代码将抛出错误,堆栈跟踪如下:

at LmbdaMain.check(LmbdaMain.java:19)
at LmbdaMain.main(LmbdaMain.java:34)
<p>

这里我们看到出错堆栈和我们代码之间是1:1的对应,这使得我们调试变得直接。

那么现在看看Scala和Java 8:

在Scala中只要使用Lambda表达式map来实现这段代码,如下:

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

如果一个错误抛出,调用栈将变得很长,如下:

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>

记住这个例子还是很简单。现实世界中嵌套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>

这非常类似Scala,出错栈信息太长,我们为代码的精简付出力代价,更精确的代码意味着更复杂的调试。

Javac必须支持Lambda函数,但是JVM还必须无视它们,这样才能保证JVM操作在一个低级别层次,而无需引入任何新的规范元素。

当你明白这个设计决定的秘密,意味着Java程序员需要付出区分调用堆栈信息的成本。这种问题在java8 + javascript上表现得更加突出。(好长的堆栈信息可见原文,点按标题进入)

本站Java8 Lambda相关文章:点按这里

[该贴被banq于2014-04-03 14:39修改过]

         

1