不要急于切換到Java 8的6個原因
Java 8是極好的。不過我們在嘗完鮮了之后,也開始對它持懷疑的態度。所有好的東西都是有代價的,在這篇文章中,我會分享Java 8的主要的幾個難點。在你放棄Java 7升級到8之前,你最好確保自己知道這些。
- 并行流會影響性能
Java 8的所承諾的并行處理是最受期待的新特性之一。集合以及流上的.parallelStream()方法就是實現這點的。它將問題分解成子問題,然后分別運 行在不同的線程上,它們可能會被分配到不同的CPU核上,當完成之后再組合起來。這些全都是在底層通過fork/join框架來實現的。好的,聽起來很酷 吧,在多核環境下的大數據集上,這么做肯定能提升操作速度的,對吧?
不,如果你用的不對的話,這么做可能會讓你的代碼變得更慢。在我們運行的基準測試上大概是慢了15%左右,而且還有可能會更糟。假設我們已經是運行 在多核環境中了,我們又使用了.parallelStream(),將更多的線程加入了線程池中。這很可能會超出我們的核數的處理能力,并且由于上下文切 換,會導致性能出現下降。
下面是我們的一個性能變差的基準測試,它是要將一個集合分到不同的組里(素數或者非素數):
Map<Boolean, List<Integer>> groupByPrimary = numbers
.parallelStream().collect(Collectors.groupingBy(s -> Utility.isPrime(s)));
還有別的原因可能會讓它變得更慢。考慮下這種情況,假設我們有多個任務要完成,其中一個可能花費的時間比其它的更長。將它用.parallelStream() 進行分解可能會導致更快的那些任務完成的時間往后推遲。看下Lukas Krecan的這篇文章,里面有更多的一些例子以及代碼。
診斷:并行處理帶來好處的同時也帶來了許多額外的問題。當你已經是處于一個多核環境中了,你要時刻牢記這點,并要弄清楚事情表面下所隱藏的本質。
- Lambda表達式的負作用
Lambda。喔,Lambda。盡管沒有你,我們也什么都可以做,但是你讓我們變得更優雅,減少了許多樣板代碼,因此大家都很容易會喜歡上你。假設一下早上我起床了想要遍歷世界杯的一組球隊,然后計算出它們的長度:
List lengths = new ArrayList();
for (String countries : Arrays.asList(args)) {
lengths.add(check(country));
}
如果有了Lambda我們就可以使用函數式了:
Stream lengths = countries.stream().map(countries -> check(country));
這太牛了。盡管很多時候它都是件好事,不過把Lambda這樣的新元素增加到Java中使得它有點偏離了最初的設計規范。字節碼是完全面向對象的,但同時這個游戲里又帶上了lambda,實際的代碼和運行時之間的差別變得越來越大了。可以讀下Tal Weiss的這篇文章,了解更多關于lambda表達式的一些陰暗面。
最后,這意味著你所寫的和你所調試的完全是兩個不同的東西。棧信息會變得越來越大,這使得你調試代碼變得更加費勁了。
將空串增加到列表里,原先只是這么簡單的一個棧信息:
at LmbdaMain.check(LmbdaMain.java:19)
at LmbdaMain.main(LmbdaMain.java:34)
現在變成了:
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)
lambda表達式引起的另一個問題就是重載:由于lambda的參數必須得強制轉化成某個類型才能進行方法調用,而它們可以轉化成好幾個類型,這可能會導致調用發生歧義。Lukas Eder通過代碼示例說明了這點。
診斷:記住這點,棧跟蹤信息可能會成為一種痛苦,不過這并不會阻擋我們使用lambda的腳步。
- 默認方法使人困惑
默認方法使得接口方法的默認實現成為了可能。這的確是Java 8帶來的一個非常酷的新特性,但是它多少影響了我們之前所習慣的做事的方式。那為什么還要引入它呢?什么時候不應該使用它?
默認方法背后最大的動機應該就是如果我們需要給現有的一個接口增加方法的話,我們可以不用重寫接口的實現。這使得它可以兼容老的版本。比如說,下面是從Oracle官方的一個Java教程中拿過來的一段代碼,它是要給一個指定的時區添加某個功能:
public interface TimeClient {
// ...
static public ZoneId getZoneId (String zoneString) {
try {
return ZoneId.of(zoneString);
} catch (DateTimeException e) {
System.err.println("Invalid time zone: " + zoneString +
"; using default time zone instead.");
return ZoneId.systemDefault();
}
}
default public ZonedDateTime getZonedDateTime(String zoneString) {
return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
}
}
問題解決了。是嗎?但默認方法將接口及實現弄得有點混淆。類型結構自己是不會糾纏到一起的,所以現在我們得好好馴服下這個新生物了。可以讀下RebelLabs的Oleg Shelajev的這篇文章。
診斷:當你掄起錘子的時候看誰都像顆釘子,記住了,要堅持原始的用例,將一個現有的接口重構成一個新的抽象類是不會有什么用處的。
下面講的這些,要么是漏掉的,要么是該刪除卻仍在的,或者是還沒有完全實現的:
- 為什么是Jigsaw
Jigsaw項目的目標是使得Java可以模塊化,并將JRE分解成能互相協作的不同組件。項目的初衷是希望Java可以更好,更快,更強地進行嵌 入。我已經盡量避免提起“物聯網”了,但剛才實際已經說到了。減少JAR包的大小,提升性能,提高安全性,這些也是這個項目的一些愿景。
那么它怎么樣了?Jigsaw目前已經通過了探索性的階段,進入了第二階段了,目前將致力于設計及實現能達到上線質量的產品,Oracle的首席架 構師Mark Reinhold如是說。這個項目原本是計劃隨著Java8發布的,后來被推遲到了Java 9,這也是9中倍受期待的新特性之一。
- 遺留的問題
受檢查異常
大家都不喜歡模板代碼,這也是為什么lambda會如此流行的原因之一。想像一下異常的樣板代碼吧,不管你是不是需要捕獲或者處理這些受檢查異常,你都得去捕獲它。盡管是根本不可能發生 的事情,就像下面這個一樣,它壓根兒就不會發生 :
try {
httpConn.setRequestMethod("GET");
}?catch (ProtocolException pe) { /* Why don’t you call me anymore? */ }
基礎類型
它們還在這里,要想正確地使用它們簡直是種痛苦。正是它使得Java無法成為一門純粹的面向對象的編程語言,并且其實上移除它們對性能也沒有太大的影響。新的JVM語言里也都沒有基礎類型。
操作符重載
Java之父James Gosling曾在一次采訪中說道:“我沒有采用操作符重載這完全是我個人的喜好,因為我看過太多人在C++中濫用它了”。這也有一定的道理,不過也有不少反對的聲音。別的JVM語言也提供了這一特性,但另一方面,它可能會導致下面這樣的代碼:
javascriptEntryPoints <<= (sourceDirectory in Compile)(base =>
((base / "assets" ** "*.js") --- (base / "assets" ** "_*")).get
)
這是Scala的Play框架中的一行真實的代碼,我已經有點崩潰了。
診斷:這些真的算是問題嗎?我們都有自己的怪癖,這些也算是Java的吧。未來的版本可能會有驚喜,這些也可能會變,但是向后兼容的問題也在那里等著我們了。
- 函數式編程
Java之前也可以進行函數式編程,雖然說有點勉強。Java 8通過lambda以及別的一些東西改進了這一狀態。這確實是最受歡迎的特性,不過并沒有之前傳說中的那么大的變化。它是比Java 7要優雅多了,不過要想成為真正的函數式語言還有很長的路要走。
關于這個問題最激烈的評論應該是來自Pierre-yves Saumont 的這一系列的文章了,他詳細比較了函數式編程的范式和Java的實現方式之間的區別。
那么該用Scala還是Java?Java采用了更現代的函數式范式也算是對Scala的一種認可,后者提供lambda也有一段時間了。lambda確實是獨領風騷,但是還有許多別的特性比如說trait,惰性求值,不可變性等,它們也是Scala不同于Java之處。
診斷: 不要被lambda分心了,Java 8算不算函數式編程仍在爭論當中。