沒有銀彈:探討 Java 8 新增特性的優缺點

Java 8或許是 迄今爲止最令人期待的Java版本,最初定於今年的9月份發佈,但由於一系列的安全漏洞問題,目前已推遲到明年的3月份。

 

Java 8試圖“創新”,根據 微軟對這個詞的定義,就是把其他框架或語言裏成熟的特性“偷”進來。在新版本發佈之前,Java社區就已經開始討論Lambda項目、Streams、函數式接口等其他好東西。下面就讓我們一起來看下這些偉大的功能,看看它們各自的優缺點,好讓你更好地應用在項目中。

Streams

集合(Collections)的改進也是Java 8的一大亮點,而讓集合越來越好的核心組件則是“Stream”。它與java.io包裏的InputStream和OutputStream是完全不同的概念,它是一個全新的概念,大家不要混淆。

此外,Stream的出現也並不是要取代ArrayLists或其他集合,它提供了一種操作大數據接口,讓數據操作更容易和更快。Stream是一次性使用對象,一旦被遍歷,就無法再次遍歷。在遍歷時,它具有過濾、映射以及減少遍歷數等功能。每個Stream都有兩種模式:順序執行和並行執行,其能夠利用多核處理器的優勢,並可以使用 fork/join並行方式來拆分任務和加速處理過程。

順序流:

1
List <Person> people = list.getStream.collect(Collectors.toList());
並行流:

1
List <Person> people = list.getStream.parallel().collect(Collectors.toList());

顧名思義,當使用順序方式去遍歷時,每個item讀完後再讀下一個item。而使用並行去遍歷時,數組會被分成多個段,其中每一個都在不同的線程中處理,然後將結果一起輸出。

並行流實例:

1
2
3
4
5
6
List originalList = someData;
split1 = originalList(0, mid);
split2 = originalList(mid,end);
new Runnable(split1.process());
new Runnable(split2.process());
List revisedList = split1 + split2;

由於一個Stream只能被遍歷一次,通常會返回另外一個Stream,可以使用終端方法(terminal method)來獲取有用的結果,終端方法可以是sum()、collect()或toArray()等。在Stream被終止之前,操作的結果不會被實現。

1
2
Double result = list.getStream().mapToDouble(f -> f.getAmount()).sum();
List<Person> people = list.getStream().filter(f -> f.getAge() > 21).collect(Collectors.toList());

該功能最大的好處是允許使用多核處理器來處理集合,這樣處理速度會更加快速。而最主要的問題則是可讀性。隨着流鏈的加長,很有可能影響可讀性。其它問題則來源於內置的新東西來支持這個新路徑,這些是功能接口和Lambda。

函數式接口

在Java 8裏將會有一個全新的功能——函數式接口(functional interfaces),就是可以在接口裏面添加默認方法,並且這些方法可以直接從接口中運行。

這樣就可以在接口中實現集合的向後兼容,並且無需改變實現這個方法的類,就可以讓Stream放置到接口中。一般而言,在接口中創建一個默認方法,然後實現該接口的所有類都可以使用Stream(無論是默認方法還是非默認方法)。

基本上就是一種多繼承形式,這樣就變成了實現者之間的問題,作爲實現人員,必須重寫這些方法,他們可以選擇使用超方法(supermethod),這也就意味着,許多實現接口的類需要改寫。

這有可能是Java 8裏最讓人關心的細節,也許Java 8裏的函數式接口對於熟悉Scala的開發者來說不算新功能,但是他們可能會拿函數式接口與Scala的特徵進行比較。然而,兩者之間不同的是:Java 8裏的函數式接口不能將一個引用指向實現類,而Scala允許通過self關鍵字來實現該操作。會有一些語言狂熱者說,Java 8裏的函數式接口只允許多繼承行爲,而不是狀態。而Scala裏的多繼承特徵既可以是行爲也可以是狀態。

在Java裏實現事務和其它項目,我們一般會使用 JavaAssist或 cglib的擴展類來構建動態代理和字節碼操作。而Scala的特行可以讓我們更直接地實現。

一方面,函數式接口可能會被以繼承方式濫用,另一方面,它們儘量不與Scala特徵重複。

Lambda

Java 8的另一大亮點是引入Lambda表達式,使用它設計的代碼會更加簡潔。當開發者在編寫Lambda表達式時,也會隨之被編譯成一個函數式接口。下面這個例子就是使用Lambda語法來代替匿名的內部類,代碼不僅簡潔,而且還可讀。

沒有使用Lambda的老方法:

1
2
3
4
5
6
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
System.out.println(“Action Detected”);
}
}
);

使用Lambda:

1
2
3
4
button.addActionListener(e -> {
System.out.println(“Action Detected”);
}
);

讓我們來看一個更明顯的例子。

不採用Lambda的老方法:

1
2
3
4
5
6
Runnable runnable1 = new Runnable() {
@Override
public void run() {
System.out.println("Running without Lambda");
}
};

使用Lambda:

1
Runnable runnable2 = () -> { System.out.println("Running from Lambda"); };

正如你所看到的,使用Lambda表達式不僅讓代碼變的簡單、而且可讀、最重要的是代碼量也隨之減少很多。然而,在某種程度上,這些功能在Scala等這些JVM語言裏已經被廣泛使用。

並不奇怪,Sclala社區是難以置信的,因爲許多Java 8裏的內容看起來就像是從Scala裏搬過來的。在某種程度上,Java 8的語法要比Scala的更詳細但不是很清晰,但這並不能說明什麼,如果可以,它可能會像Scala那樣構建Lambda表達式。

一方面,如果Java繼續圍繞Lambda來發展和實現Scala都已經實現的功能,那麼可能就不需要Scala了。另一方面,如果它只提供一些核心的功能,例如幫助匿名內部類,那麼Scala和其他語言將會繼續茁壯成長,並且有可能會凌駕於Java之上。其實這纔是最好的結果,有競爭纔有進步,其它語言繼續發展和成長,並且無需擔心是否會過時。

Java time

Time在Java裏已有很長一段時間,首先出現的java.util.Date這個包,其次還有java.sql.Date、Calendar。但處理時間和日期需要大量的monkey代碼,因此,像Joda Time等第三方庫因此誕生。姍姍來遲,Oracle終於決定在Java裏添加一個 java.time包來清理各種時間接口。它看起來很符合現在開發者的胃口,擁有各種各樣的時間API。

Java API可以處理一些時空連續體方面的特性,比如距離、質量、重量等,這是值得稱讚的,但我仍然認爲 Currency會處理得更好。我認爲Java API需要好好地修剪而不是添加更多的東西,並且首先Java API應該對這些基本元素提供標準的兼容。

Nashorn

Nashorn是Rhino的接替者,該項目的目的是基於Java實現一個輕量級高性能的JavaScript運行環境。

JDK 7中添加了invokeDynamic,其主要是用來支持非Java語言,尤其是動態語言。而JDK 8中的Nashorn將會給開發者提供一個更加實用的JavaScript實現。事實上,Oracle已經有了他自己的Node.js實現,叫做Node.jar。這似乎比在Java裏運行JavaScript更加吸引人。

Accumulators

自從JDK中集成了 java.util.concurrent以來,該特性並沒有停止發展。相反,JDK 8將構建於JDK 7和fork/join框架之上,並通過加法器(adders)和累加器(Accumulators)得到了進一步的發展。

首先是同步。但是,如果你使用同步在多線程之間進行增量計數,那麼同步有可能難以負擔。在Java 6中通過讓非競爭鎖更廉價(cheap)來使同步不那麼難以負擔。其中大多數會使用Vector來提升老應用程序性能,幾乎每一個單線程都受到了Java Activation Framework的影響。

Java.util.concurrent包使得線程池和其他相對複雜的多線程結構變得更好,但是,倘若你想要通過跨線程來增加一個變量,那麼就有點大材小用了。對此,我們採用一種比真正的鎖更輕更快的原子。在JDK 8中,我們採用Accumulators和adders,這些要比原子輕量多了,對於大多數異構代碼來說,這些足以滿足它們的需求,如果線程太多,那麼可以增加一個計數器。但想要看到類似map/reduce實現或統計跨線程之間的總和,你仍然需要使用原子,因爲如果要讀取這些跨線程的值,累積的順序是無法得以保證的。

HashMap修復

在Java中使用String.hashCode()實現已是大家熟知的bug。如果在特定的代碼中引入HashMap,可能會導致拒絕服務攻擊。基本上,如果有足夠多的參數hash到相同值,那麼可能會消耗過多的CPU時間。

通常,HashMap bucket採用鏈表的方式來存儲map條目。使用此算法存在大量的衝突,並且增加了O(1)到O(N)這種哈希變化的複雜性,爲了解決這一問題,通過採用平衡tree算法來降低複雜度。

TLS SNI

SNI是 服務器名稱標識(Server Name Identification)的縮寫,由於大多數公共網站的訪客數量不是太多,幾乎很少能達到數百萬用戶。很多網站都使用相同的IP地址和基於名字的虛擬主機,比如我訪問 podcasts.infoworld.com和 www.infoworld.com,最後的網址是一樣的,但訪問的主機名是不一樣的,所以我有可能會訪問到不同的Web頁面。然而,因爲SSL,我可能無法分享IP地址。由於HTTP主機頭是建立在基於命名的虛擬主機上,並且主機也是依賴SSL來實現加密/解密的,所以,不得不爲每個SSL證書申請不同的IP地址。

在最近幾年都是採用SNI來解決這一問題的,Java也不例外。這種方式得到了大多數瀏覽器的支持,現在Apache和Java也支持它。這意味着過不了多久,我們就可以看到Apache和基於Java的服務器使用Oracle的SSL實現來支持SNI,稱作 JSSE

總結

總之,Java 8包含了一大堆非常實用的特性,這也是許多開發者想使用最新版本的原因之一。在我看來,Stream是最好的一個特性。但願並行集合也能夠爲其進程性能帶來一些提升。而函數式接口可能並不會像預期中的那樣好用,萬一使用不當,可能會給開發者帶來很多麻煩。

本文只是總結了部分Java 8新特性,我們相信,在發佈的時候將會有更多新特性與大家見面。你可以通過Simon Ritter在JavaOne 2013大會上的演講PPT來了解目前已經添加到Java 8中的55個新特性。

至於該如何取捨,各位開發者應該根據自己的實際需求去研究和使用,並不是所有的新特性就是好的,它們也存在優缺點。(編譯:張紅月/責編:王果)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章