不要急於切換到Java 8的6個原因

deepinmind發表於2014-07-11

  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算不算函數語言程式設計仍在爭論當中。

  英文原文:6-reasons-not-to-switch-to-java-8-just-yet

相關文章