Java 8新特性終極指南
摘自http://www.importnew.com/11908.html
編者注:Java 8已經公布有一段時間了,種種跡象表明Java 8是一個有重大改變的發行版。
在Java Code Geeks上已經有大量的關於Java 8 的教程了,像玩轉Java 8——lambda與並發,Java 8 Date Time API 教程: LocalDateTime和在Java 8中抽象類與接口的比較。
我們也在其他地方引用了15個必讀的Java 8教程。當然,我們也探究了Java 8的一些不足之處,比如Java 8的“黑暗面”。
現在,是時候把所有Java 8的重要特性收集整理成一篇單獨的文章了,希望這篇文章能給你帶來閱讀上的樂趣。開始吧!
目錄結構
- 介紹
- Java語言的新特性
2.2 接口的默認與靜態方法
2.3 方法引用
2.4 重復注解
2.5 更好的類型推測機制
2.6 擴展注解的支持
- Java編譯器的新特性
3.1 參數名字
- Java 類庫的新特性
4.1 Optional
4.2 Streams
4.5 Base64
4.6 並行(parallel)數組
4.7 並發(Concurrency)
- 新增的Java工具
5.1 Nashorn引擎: jjs
5.2 類依賴分析器: jdeps
1.介紹
毫無疑問,Java 8發行版是自Java 5(發行於2004,已經過了相當一段時間了)以來最具革命性的版本。Java 8 為Java語言、編譯器、類庫、開發工具與JVM(Java虛擬機)帶來了大量新特性。在這篇教程中,我們將一一探索這些變化,並用真實的例子說明它們適用的場景。
這篇教程由以下幾部分組成,它們分別涉及到Java平台某一特定方面的內容:
- Java語言
- 編譯器
- 類庫
- 工具
- Java運行時(JVM)
2.Java語言的新特性
不管怎么說,Java 8都是一個變化巨大的版本。你可能認為Java 8耗費了大量的時間才得以完成是為了實現了每個Java程序員所期待的特性。在這個小節里,我們將會涉及到這些特性的大部分。
2.1 Lambda表達式與Functional接口
Lambda表達式(也稱為閉包)是整個Java 8發行版中最受期待的在Java語言層面上的改變,Lambda允許把函數作為一個方法的參數(函數作為參數傳遞進方法中),或者把代碼看成數據:函數式程序員對這一概念非常熟悉。在JVM平台上的很多語言(Groovy,Scala,……)從一開始就有Lambda,但是Java程序員不得不使用毫無新意的匿名類來代替lambda。
關於Lambda設計的討論占用了大量的時間與社區的努力。可喜的是,最終找到了一個平衡點,使得可以使用一種即簡潔又緊湊的新方式來構造Lambdas。在最簡單的形式中,一個lambda可以由用逗號分隔的參數列表、–>符號與函數體三部分表示。例如:
1
|
Arrays.asList(
"a"
,
"b"
,
"d"
).forEach( e -> System.out.println( e ) );
|
請注意參數e的類型是由編譯器推測出來的。同時,你也可以通過把參數類型與參數包括在括號中的形式直接給出參數的類型:
1
|
Arrays.asList(
"a"
,
"b"
,
"d"
).forEach( ( String e ) -> System.out.println( e ) );
|
在某些情況下lambda的函數體會更加復雜,這時可以把函數體放到在一對花括號中,就像在Java中定義普通函數一樣。例如:
1
2
3
4
|
Arrays.asList(
"a"
,
"b"
,
"d"
).forEach( e -> {
System.out.print( e );
System.out.print( e );
} );
|
Lambda可以引用類的成員變量與局部變量(如果這些變量不是final的話,它們會被隱含的轉為final,這樣效率更高)。例如,下面兩個代碼片段是等價的:
1
2
3
|
String separator =
","
;
Arrays.asList(
"a"
,
"b"
,
"d"
).forEach(
( String e ) -> System.out.print( e + separator ) );
|
和:
1
2
3
|
final
String separator =
","
;
Arrays.asList(
"a"
,
"b"
,
"d"
).forEach(
( String e ) -> System.out.print( e + separator ) );
|
Lambda可能會返回一個值。返回值的類型也是由編譯器推測出來的。如果lambda的函數體只有一行的話,那么沒有必要顯式使用return語句。下面兩個代碼片段是等價的:
1
|
Arrays.asList(
"a"
,
"b"
,
"d"
).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );
|
和:
1
2
3
4
|
Arrays.asList(
"a"
,
"b"
,
"d"
).sort( ( e1, e2 ) -> {
int
result = e1.compareTo( e2 );
return
result;
} );
|
語言設計者投入了大量精力來思考如何使現有的函數友好地支持lambda。最終采取的方法是:增加函數式接口的概念。函數式接口就是一個具有一個方法的普通接口。像這樣的接口,可以被隱式轉換為lambda表達式。java.lang.Runnable與java.util.concurrent.Callable是函數式接口最典型的兩個例子。在實際使用過程中,函數式接口是容易出錯的:如有某個人在接口定義中增加了另一個方法,這時,這個接口就不再是函數式的了,並且編譯過程也會失敗。為了克服函數式接口的這種脆弱性並且能夠明確聲明接口作為函數式接口的意圖,Java 8增加了一種特殊的注解@FunctionalInterface(Java 8中所有類庫的已有接口都添加了@FunctionalInterface注解)。讓我們看一下這種函數式接口的定義:
1
2
3
4
|
@FunctionalInterface
public
interface
Functional {
void
method();
}
|
需要記住的一件事是:默認方法與靜態方法並不影響函數式接口的契約,可以任意使用:
1
2
3
4
5
6
7
|
@FunctionalInterface
public
interface
FunctionalDefaultMethods {
void
method();
default
void
defaultMethod() {
}
}
|
Lambda是Java 8最大的賣點。它具有吸引越來越多程序員到Java平台上的潛力,並且能夠在純Java語言環境中提供一種優雅的方式來支持函數式編程。更多詳情可以參考官方文檔。
2.2 接口的默認方法與靜態方法
Java 8用默認方法與靜態方法這兩個新概念來擴展接口的聲明。默認方法使接口有點像Traits(Scala中特征(trait)類似於Java中的Interface,但它可以包含實現代碼,也就是目前Java8新增的功能),但與傳統的接口又有些不一樣,它允許在已有的接口中添加新方法,而同時又保持了與舊版本代碼的兼容性。
默認方法與抽象方法不同之處在於抽象方法必須要求實現,但是默認方法則沒有這個要求。相反,每個接口都必須提供一個所謂的默認實現,這樣所有的接口實現者將會默認繼承它(如果有必要的話,可以覆蓋這個默認實現)。讓我們看看下面的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
private
interface
Defaulable {
// Interfaces now allow default methods, the implementer may or
// may not implement (override) them.
default
String notRequired() {
return
"Default implementation"
;
}
}
private
static
class
DefaultableImpl
implements
Defaulable {
}
private
static
class
OverridableImpl
implements
Defaulable {
@Override
public
String notRequired() {
return
"Overridden implementation"
;
}
}
|
Defaulable接口用關鍵字default聲明了一個默認方法notRequired(),Defaulable接口的實現者之一DefaultableImpl實現了這個接口,並且讓默認方法保持原樣。Defaulable接口的另一個實現者OverridableImpl用自己的方法覆蓋了默認方法。
Java 8帶來的另一個有趣的特性是接口可以聲明(並且可以提供實現)靜態方法。例如:
1
2
3
4
5
6
|
private
interface
DefaulableFactory {
// Interfaces now allow static methods
static
Defaulable create( Supplier< Defaulable > supplier ) {
return
supplier.get();
}
}
|
下面的一小段代碼片段把上面的默認方法與靜態方法黏合到一起。
1
2
3
4
5
6
7
|
public
static
void
main( String[] args ) {
Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::
new
);
System.out.println( defaulable.notRequired() );
defaulable = DefaulableFactory.create( OverridableImpl::
new
);
System.out.println( defaulable.notRequired() );
}
|
這個程序的控制台輸出如下:
1
2
|
Default implementation
Overridden implementation
|
在JVM中,默認方法的實現是非常高效的,並且通過字節碼指令為方法調用提供了支持。默認方法允許繼續使用現有的Java接口,而同時能夠保障正常的編譯過程。這方面好的例子是大量的方法被添加到java.util.Collection接口中去:stream(),parallelStream(),forEach(),removeIf(),……
盡管默認方法非常強大,但是在使用默認方法時我們需要小心注意一個地方:在聲明一個默認方法前,請仔細思考是不是真的有必要使用默認方法,因為默認方法會帶給程序歧義,並且在復雜的繼承體系中容易產生編譯錯誤。更多詳情請參考官方文檔
2.3 方法引用
方法引用提供了非常有用的語法,可以直接引用已有Java類或對象(實例)的方法或構造器。與lambda聯合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗余代碼。
下面,我們以定義了4個方法的Car這個類作為例子,區分Java中支持的4種不同的方法引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
static
class
Car {
public
static
Car create(
final
Supplier< Car > supplier ) {
return
supplier.get();
}
public
static
void
collide(
final
Car car ) {
System.out.println(
"Collided "
+ car.toString() );
}
public
void
follow(
final
Car another ) {
System.out.println(
"Following the "
+ another.toString() );
}
public
void
repair() {
System.out.println(
"Repaired "
+
this
.toString() );
}
}
|
第一種方法引用是構造器引用,它的語法是Class::new,或者更一般的Class< T >::new。請注意構造器沒有參數。
1
2
|
final
Car car = Car.create( Car::
new
);
final
List< Car > cars = Arrays.asList( car );
|
第二種方法引用是靜態方法引用,它的語法是Class::static_method。請注意這個方法接受一個Car類型的參數。
1
|
cars.forEach( Car::collide );
|
第三種方法引用是特定類的任意對象的方法引用,它的語法是Class::method。請注意,這個方法沒有參數。
1
|
cars.forEach( Car::repair );
|
最后,第四種方法引用是特定對象的方法引用,它的語法是instance::method。請注意,這個方法接受一個Car類型的參數
1
2
|
final
Car police = Car.create( Car::
new
);
cars.forEach( police::follow );
|
運行上面的Java程序在控制台上會有下面的輸出(Car的實例可能不一樣):
1
2
3
|
Collided com.javacodegeeks.java8.method.references.MethodReferences$Car
@7a81197d
Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car
@7a81197d
Following the com.javacodegeeks.java8.method.references.MethodReferences$Car
@7a81197d
|
關於方法引用的更多詳情請參考官方文檔。
2.4 重復注解
自從Java 5引入了注解機制,這一特性就變得非常流行並且廣為使用。然而,使用注解的一個限制是相同的注解在同一位置只能聲明一次,不能聲明多次。Java 8打破了這條規則,引入了重復注解機制,這樣相同的注解可以在同一地方聲明多次。
重復注解機制本身必須用@Repeatable注解。事實上,這並不是語言層面上的改變,更多的是編譯器的技巧,底層的原理保持不變。讓我們看一個快速入門的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
package
com.javacodegeeks.java8.repeatable.annotations;
import
java.lang.annotation.ElementType;
import
java.lang.annotation.Repeatable;
import
java.lang.annotation.Retention;
import
java.lang.annotation.RetentionPolicy;
import
java.lang.annotation.Target;
public
class
RepeatingAnnotations {
@Target
( ElementType.TYPE )
@Retention
( RetentionPolicy.RUNTIME )
public
@interface
Filters {
Filter[] value();
}
@Target
( ElementType.TYPE )
@Retention
( RetentionPolicy.RUNTIME )
@Repeatable
( Filters.
class
)
public
@interface
Filter {
String value();
};
@Filter
(
"filter1"
)
@Filter
(
"filter2"
)
public
interface
Filterable {
}
public
static
void
main(String[] args) {
for
( Filter filter: Filterable.
class
.getAnnotationsByType( Filter.
class
) ) {
System.out.println( filter.value() );
}
}
}
|
正如我們看到的,這里有個使用@Repeatable( Filters.class )注解的注解類Filter,Filters僅僅是Filter注解的數組,但Java編譯器並不想讓程序員意識到Filters的存在。這樣,接口Filterable就擁有了兩次Filter(並沒有提到Filter)注解。
同時,反射相關的API提供了新的函數getAnnotationsByType()來返回重復注解的類型(請注意Filterable.class.getAnnotation( Filters.class )經編譯器處理后將會返回Filters的實例)。
程序輸出結果如下:
1
2
|
filter1
filter2
|
更多詳情請參考官方文檔
2.5 更好的類型推測機制
Java 8在類型推測方面有了很大的提高。在很多情況下,編譯器可以推測出確定的參數類型,這樣就能使代碼更整潔。讓我們看一個例子:
1
2
3
4
5
6
7
8
9
10
11
|
package
com.javacodegeeks.java8.type.inference;
public
class
Value< T > {
public
static
< T > T defaultValue() {
return
null
;
}
public
T getOrDefault( T value, T defaultValue ) {
return
( value !=
null
) ? value : defaultValue;
}
}
|
這里是Value< String >類型的用法。
1
2
3
4
5
6
7
8
|
package
com.javacodegeeks.java8.type.inference;
public
class
TypeInference {
public
static
void
main(String[] args) {
final
Value< String > value =
new
Value<>();
value.getOrDefault(
"22"
, Value.defaultValue() );
}
}
|
Value.defaultValue()的參數類型可以被推測出,所以就不必明確給出。在Java 7中,相同的例子將不會通過編譯,正確的書寫方式是 Value.< String >defaultValue()。
2.6 擴展注解的支持
Java 8擴展了注解的上下文。現在幾乎可以為任何東西添加注解:局部變量、泛型類、父類與接口的實現,就連方法的異常也能添加注解。下面演示幾個例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package
com.javacodegeeks.java8.annotations;
import
java.lang.annotation.ElementType;
import
java.lang.annotation.Retention;
import
java.lang.annotation.RetentionPolicy;
import
java.lang.annotation.Target;
import
java.util.ArrayList;
import
java.util.Collection;
public
class
Annotations {
@Retention
( RetentionPolicy.RUNTIME )
@Target
( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } )
public
@interface
NonEmpty {
}
public
static
class
Holder<
@NonEmpty
T >
extends
@NonEmpty
Object {
public
void
method()
throws
@NonEmpty
Exception {
}
}
@SuppressWarnings
(
"unused"
)
public
static
void
main(String[] args) {
final
Holder< String > holder =
new
@NonEmpty
Holder< String >();
@NonEmpty
Collection<
@NonEmpty
String > strings =
new
ArrayList<>();
}
}
|
ElementType.TYPE_USE和ElementType.TYPE_PARAMETER是兩個新添加的用於描述適當的注解上下文的元素類型。在Java語言中,注解處理API也有小的改動來識別新增的類型注解。
3. Java編譯器的新特性
3.1 參數名字
很長一段時間里,Java程序員一直在發明不同的方式使得方法參數的名字能保留在Java字節碼中,並且能夠在運行時獲取它們(比如,Paranamer類庫)。最終,在Java 8中把這個強烈要求的功能添加到語言層面(通過反射API與Parameter.getName()方法)與字節碼文件(通過新版的javac的–parameters選項)中。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package
com.javacodegeeks.java8.parameter.names;
import
java.lang.reflect.Method;
import
java.lang.reflect.Parameter;
public
class
ParameterNames {
public
static
void
main(String[] args)
throws
Exception {
Method method = ParameterNames.
class
.getMethod(
"main"
, String[].
class
);
for
(
final
Parameter parameter: method.getParameters() ) {
System.out.println(
"Parameter: "
+ parameter.getName() );
}
}
}
|
如果不使用–parameters參數來編譯這個類,然后運行這個類,會得到下面的輸出:
1
|
Parameter: arg0
|
如果使用–parameters參數來編譯這個類,程序的結構會有所不同(參數的真實名字將會顯示出來):
1
|
Parameter: args
|
對於有經驗的Maven用戶,通過maven-compiler-plugin的配置可以將-parameters參數添加到編譯器中去。
1
2
3
4
5
6
7
8
9
10
|
<
plugin
>
<
groupId
>org.apache.maven.plugins</
groupId
>
<
artifactId
>maven-compiler-plugin</
artifactId
>
<
version
>3.1</
version
>
<
configuration
>
<
compilerArgument
>-parameters</
compilerArgument
>
<
source
>1.8</
source
>
<
target
>1.8</
target
>
</
configuration
>
</
plugin
>
|
針對Java 8最新發布的Eclipse Kepler SR2(請檢查這里的下載說明)提供了非常實用的配置選項,可以通過下圖的配置方式來控制編譯器行為
此外,Parameter類有一個很方便的方法isNamePresent()來驗證是否可以獲取參數的名字。
4. Java 類庫的新特性
Java 8 通過增加大量新類,擴展已有類的功能的方式來改善對並發編程、函數式編程、日期/時間相關操作以及其他更多方面的支持。
4.1 Optional
到目前為止,臭名昭著的空指針異常是導致Java應用程序失敗的最常見原因。以前,為了解決空指針異常,Google公司著名的Guava項目引入了Optional類,Guava通過使用檢查空值的方式來防止代碼污染,它鼓勵程序員寫更干凈的代碼。受到Google Guava的啟發,Optional類已經成為Java 8類庫的一部分。
Optional實際上是個容器:它可以保存類型T的值,或者僅僅保存null。Optional提供很多有用的方法,這樣我們就不用顯式進行空值檢測。更多詳情請參考官方文檔。
我們下面用兩個小例子來演示如何使用Optional類:一個允許為空值,一個不允許為空值。
1
2
3
4
|
Optional< String > fullName = Optional.ofNullable(
null
);
System.out.println(
"Full Name is set? "
+ fullName.isPresent() );
System.out.println(
"Full Name: "
+ fullName.orElseGet( () ->
"[none]"
) );
System.out.println( fullName.map( s ->
"Hey "
+ s +
"!"
).orElse(
"Hey Stranger!"
) );
|
如果Optional類的實例為非空值的話,isPresent()返回true,否從返回false。為了防止Optional為空值,orElseGet()方法通過回調函數來產生一個默認值。map()函數對當前Optional的值進行轉化,然后返回一個新的Optional實例。orElse()方法和orElseGet()方法類似,但是orElse接受一個默認值而不是一個回調函數。下面是這個程序的輸出:
1
2
3
|
Full Name is
set
?
false
Full Name: [none]
Hey Stranger!
|
讓我們來看看另一個例子:
1
2
3
4
5
|
Optional< String > firstName = Optional.of(
"Tom"
);
System.out.println(
"First Name is set? "
+ firstName.isPresent() );
System.out.println(
"First Name: "
+ firstName.orElseGet( () ->
"[none]"
) );
System.out.println( firstName.map( s ->
"Hey "
+ s +
"!"
).orElse(
"Hey Stranger!"
) );
System.out.println();
|
下面是程序的輸出:
1
2
3
|
First Name is
set
?
true
First Name: Tom
Hey Tom!
|
更多詳情請參考官方文檔
4.2 Stream
最新添加的Stream API(java.util.stream) 把真正的函數式編程風格引入到Java中。這是目前為止對Java類庫最好的補充,因為Stream API可以極大提供Java程序員的生產力,讓程序員寫出高效率、干凈、簡潔的代碼。
Stream API極大簡化了集合框架的處理(但它的處理的范圍不僅僅限於集合框架的處理,這點后面我們會看到)。讓我們以一個簡單的Task類為例進行介紹:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public
class
Streams {
private
enum
Status {
OPEN, CLOSED
};
private
static
final
class
Task {
private
final
Status status;
private
final
Integer points;
Task(
final
Status status,
final
Integer points ) {
this
.status = status;
this
.points = points;
}
public
Integer getPoints() {
return
points;
}
public
Status getStatus() {
return
status;
}
@Override
public
String toString() {
return
String.format(
"[%s, %d]"
, status, points );
}
}
}
|
Task類有一個分數的概念(或者說是偽復雜度),其次是還有一個值可以為OPEN或CLOSED的狀態.讓我們引入一個Task的小集合作為演示例子:
1
2
3
4
5
|
final
Collection< Task > tasks = Arrays.asList(
new
Task( Status.OPEN,
5
),
new
Task( Status.OPEN,
13
),
new
Task( Status.CLOSED,
8
)
);
|
我們下面要討論的第一個問題是所有狀態為OPEN的任務一共有多少分數?在Java 8以前,一般的解決方式用foreach循環,但是在Java 8里面我們可以使用stream:一串支持連續、並行聚集操作的元素。
1
2
3
4
5
6
7
8
|
// Calculate total points of all active tasks using sum()
final
long
totalPointsOfOpenTasks = tasks
.stream()
.filter( task -> task.getStatus() == Status.OPEN )
.mapToInt( Task::getPoints )
.sum();
System.out.println(
"Total points: "
+ totalPointsOfOpenTasks );
|
程序在控制台上的輸出如下:
1
|
Total points: 18
|
這里有幾個注意事項。第一,task集合被轉換化為其相應的stream表示。然后,filter操作過濾掉狀態為CLOSED的task。下一步,mapToInt操作通過Task::getPoints這種方式調用每個task實例的getPoints方法把Task的stream轉化為Integer的stream。最后,用sum函數把所有的分數加起來,得到最終的結果。
在繼續講解下面的例子之前,關於stream有一些需要注意的地方(詳情在這里).stream操作被分成了中間操作與最終操作這兩種。
中間操作返回一個新的stream對象。中間操作總是采用惰性求值方式,運行一個像filter這樣的中間操作實際上沒有進行任何過濾,相反它在遍歷元素時會產生了一個新的stream對象,這個新的stream對象包含原始stream
中符合給定謂詞的所有元素。
像forEach、sum這樣的最終操作可能直接遍歷stream,產生一個結果或副作用。當最終操作執行結束之后,stream管道被認為已經被消耗了,沒有可能再被使用了。在大多數情況下,最終操作都是采用及早求值方式,及早完成底層數據源的遍歷。
stream另一個有價值的地方是能夠原生支持並行處理。讓我們來看看這個算task分數和的例子。
1
2
3
4
5
6
7
8
|
// Calculate total points of all tasks
final
double
totalPoints = tasks
.stream()
.parallel()
.map( task -> task.getPoints() )
// or map( Task::getPoints )
.reduce(
0
, Integer::sum );
System.out.println(
"Total points (all tasks): "
+ totalPoints );
|
這個例子和第一個例子很相似,但這個例子的不同之處在於這個程序是並行運行的,其次使用reduce方法來算最終的結果。
下面是這個例子在控制台的輸出:
1
|
Total points (all tasks): 26.0
|
經常會有這個一個需求:我們需要按照某種准則來對集合中的元素進行分組。Stream也可以處理這樣的需求,下面是一個例子:
1
2
3
4
5
|
// Group tasks by their status
final
Map< Status, List< Task > > map = tasks
.stream()
.collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );
|
這個例子的控制台輸出如下:
1
|
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
|
讓我們來計算整個集合中每個task分數(或權重)的平均值來結束task的例子。
1
2
3
4
5
6
7
8
9
10
11
12
|
// Calculate the weight of each tasks (as percent of total points)
final
Collection< String > result = tasks
.stream()
// Stream< String >
.mapToInt( Task::getPoints )
// IntStream
.asLongStream()
// LongStream
.mapToDouble( points -> points / totalPoints )
// DoubleStream
.boxed()
// Stream< Double >
.mapToLong( weigth -> (
long
)( weigth *
100
) )
// LongStream
.mapToObj( percentage -> percentage +
"%"
)
// Stream< String>
.collect( Collectors.toList() );
// List< String >
System.out.println( result );
|
下面是這個例子的控制台輸出:
1
|
[19%, 50%, 30%]
|
最后,就像前面提到的,Stream API不僅僅處理Java集合框架。像從文本文件中逐行讀取數據這樣典型的I/O操作也很適合用Stream API來處理。下面用一個例子來應證這一點。
1
2
3
4
|
final
Path path =
new
File( filename ).toPath();
try
( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
lines.onClose( () -> System.out.println(
"Done!"
) ).forEach( System.out::println );
}
|
對一個stream對象調用onClose方法會返回一個在原有功能基礎上新增了關閉功能的stream對象,當對stream對象調用close()方法時,與關閉相關的處理器就會執行。
Stream API、Lambda表達式與方法引用在接口默認方法與靜態方法的配合下是Java 8對現代軟件開發范式的回應。更多詳情請參考官方文檔。
4.3 Date/Time API (JSR 310)
Java 8通過發布新的Date-Time API (JSR 310)來進一步加強對日期與時間的處理。對日期與時間的操作一直是Java程序員最痛苦的地方之一。標准的 java.util.Date以及后來的java.util.Calendar一點沒有改善這種情況(可以這么說,它們一定程度上更加復雜)。
這種情況直接導致了Joda-Time——一個可替換標准日期/時間處理且功能非常強大的Java API的誕生。Java 8新的Date-Time API (JSR 310)在很大程度上受到Joda-Time的影響,並且吸取了其精髓。新的java.time包涵蓋了所有處理日期,時間,日期/時間,時區,時刻(instants),過程(during)與時鍾(clock)的操作。在設計新版API時,十分注重與舊版API的兼容性:不允許有任何的改變(從java.util.Calendar中得到的深刻教訓)。如果需要修改,會返回這個類的一個新實例。
讓我們用例子來看一下新版API主要類的使用方法。第一個是Clock類,它通過指定一個時區,然后就可以獲取到當前的時刻,日期與時間。Clock可以替換System.currentTimeMillis()與TimeZone.getDefault()。
1
2
3
4
|
// Get the system clock as UTC offset
final
Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );
|
下面是程序在控制台上的輸出:
1
2
|
2014-04-12T15:19:29.282Z
1397315969360
|
我們需要關注的其他類是LocaleDate與LocalTime。LocaleDate只持有ISO-8601格式且無時區信息的日期部分。相應的,LocaleTime只持有ISO-8601格式且無時區信息的時間部分。LocaleDate與LocalTime都可以從Clock中得到。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// Get the local date and local time
final
LocalDate date = LocalDate.now();
final
LocalDate dateFromClock = LocalDate.now( clock );
System.out.println( date );
System.out.println( dateFromClock );
// Get the local date and local time
final
LocalTime time = LocalTime.now();
final
LocalTime timeFromClock = LocalTime.now( clock );
System.out.println( time );
System.out.println( timeFromClock );
|
下面是程序在控制台上的輸出:
1
2
3
4
|
2014-04-12
2014-04-12
11:25:54.568
15:25:54.568
|
LocaleDateTime把LocaleDate與LocaleTime的功能合並起來,它持有的是ISO-8601格式無時區信息的日期與時間。下面是一個快速入門的例子。
1
2
3
4
5
6
|
// Get the local date/time
final
LocalDateTime datetime = LocalDateTime.now();
final
LocalDateTime datetimeFromClock = LocalDateTime.now( clock );
System.out.println( datetime );
System.out.println( datetimeFromClock );
|
下面是程序在控制台上的輸出:
1
2
|
2014-04-12T11:37:52.309
2014-04-12T15:37:52.309
|
如果你需要特定時區的日期/時間,那么ZonedDateTime是你的選擇。它持有ISO-8601格式具具有時區信息的日期與時間。下面是一些不同時區的例子:
1
2
3
4
5
6
7
8
|
//
Get the zoned
date
/time
final ZonedDateTime zonedDatetime = ZonedDateTime.now();
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock );
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of(
"America/Los_Angeles"
) );
System.out.println( zonedDatetime );
System.out.println( zonedDatetimeFromClock );
System.out.println( zonedDatetimeFromZone );
|
下面是程序在控制台上的輸出:
1
2
3
|
2014-04-12T11:47:01.017-04:00[America
/New_York
]
2014-04-12T15:47:01.017Z
2014-04-12T08:47:01.017-07:00[America
/Los_Angeles
]
|
最后,讓我們看一下Duration類:在秒與納秒級別上的一段時間。Duration使計算兩個日期間的不同變的十分簡單。下面讓我們看一個這方面的例子。
1
2
3
4
5
6
7
|
// Get duration between two dates
final
LocalDateTime from = LocalDateTime.of(
2014
, Month.APRIL,
16
,
0
,
0
,
0
);
final
LocalDateTime to = LocalDateTime.of(
2015
, Month.APRIL,
16
,
23
,
59
,
59
);
final
Duration duration = Duration.between( from, to );
System.out.println(
"Duration in days: "
+ duration.toDays() );
System.out.println(
"Duration in hours: "
+ duration.toHours() );
|
上面的例子計算了兩個日期2014年4月16號與2014年4月16號之間的過程。下面是程序在控制台上的輸出:
1
2
|
Duration
in
days: 365
Duration
in
hours: 8783
|
對Java 8在日期/時間API的改進整體印象是非常非常好的。一部分原因是因為它建立在“久戰殺場”的Joda-Time基礎上,另一方面是因為用來大量的時間來設計它,並且這次程序員的聲音得到了認可。更多詳情請參考官方文檔。
4.4 JavaScript引擎Nashorn
Nashorn,一個新的JavaScript引擎隨着Java 8一起公諸於世,它允許在JVM上開發運行某些JavaScript應用。Nashorn就是javax.script.ScriptEngine的另一種實現,並且它們倆遵循相同的規則,允許Java與JavaScript相互調用。下面看一個例子:
1
2
3
4
5
|
ScriptEngineManager manager =
new
ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(
"JavaScript"
);
System.out.println( engine.getClass().getName() );
System.out.println(
"Result:"
+ engine.eval(
"function f() { return 1; }; f() + 1;"
) );
|
下面是程序在控制台上的輸出:
1
2
|
jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2
|
4.5 Base64
在Java 8中,Base64編碼已經成為Java類庫的標准。它的使用十分簡單,下面讓我們看一個例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package
com.javacodegeeks.java8.base64;
import
java.nio.charset.StandardCharsets;
import
java.util.Base64;
public
class
Base64s {
public
static
void
main(String[] args) {
final
String text =
"Base64 finally in Java 8!"
;
final
String encoded = Base64
.getEncoder()
.encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
System.out.println( encoded );
final
String decoded =
new
String(
Base64.getDecoder().decode( encoded ),
StandardCharsets.UTF_8 );
System.out.println( decoded );
}
}
|
程序在控制台上輸出了編碼后的字符與解碼后的字符:
1
2
|
QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally
in
Java 8!
|
Base64類同時還提供了對URL、MIME友好的編碼器與解碼器(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。
4.6 並行(parallel)數組
Java 8增加了大量的新方法來對數組進行並行處理。可以說,最重要的是parallelSort()方法,因為它可以在多核機器上極大提高數組排序的速度。下面的例子展示了新方法(parallelXxx)的使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package
com.javacodegeeks.java8.parallel.arrays;
import
java.util.Arrays;
import
java.util.concurrent.ThreadLocalRandom;
public
class
ParallelArrays {
public
static
void
main( String[] args ) {
long
[] arrayOfLong =
new
long
[
20000
];
Arrays.parallelSetAll( arrayOfLong,
index -> ThreadLocalRandom.current().nextInt(
1000000
) );
Arrays.stream( arrayOfLong ).limit(
10
).forEach(
i -> System.out.print( i +
" "
) );
System.out.println();
Arrays.parallelSort( arrayOfLong );
Arrays.stream( arrayOfLong ).limit(
10
).forEach(
i -> System.out.print( i +
" "
) );
System.out.println();
}
}
|
上面的代碼片段使用了parallelSetAll()方法來對一個有20000個元素的數組進行隨機賦值。然后,調用parallelSort方法。這個程序首先打印出前10個元素的值,之后對整個數組排序。這個程序在控制台上的輸出如下(請注意數組元素是隨機生產的):
1
2
|
Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793
|
4.7 並發(Concurrency)
在新增Stream機制與lambda的基礎之上,在java.util.concurrent.ConcurrentHashMap中加入了一些新方法來支持聚集操作。同時也在java.util.concurrent.ForkJoinPool類中加入了一些新方法來支持共有資源池(common pool)(請查看我們關於Java 並發的免費課程)。
新增的java.util.concurrent.locks.StampedLock類提供一直基於容量的鎖,這種鎖有三個模型來控制讀寫操作(它被認為是不太有名的java.util.concurrent.locks.ReadWriteLock類的替代者)。
在java.util.concurrent.atomic包中還增加了下面這些類:
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
5. 新的Java工具
Java 8也帶來了一些新的命令行工具。在這節里我們將會介紹它們中最有趣的部分。
5.1 Nashorn引擎: jjs
jjs是個基於Nashorn引擎的命令行工具。它接受一些JavaScript源代碼為參數,並且執行這些源代碼。例如,我們創建一個具有如下內容的func.js文件:
1
2
3
4
5
|
function
f() {
return
1;
};
print( f() + 1 );
|
我們可以把這個文件作為參數傳遞給jjs使得這個文件可以在命令行中執行:
1
|
jjs func.js
|
下面是程序在控制台上的輸出:
1
|
2
|
更多詳情請參考官方文檔
5.2 類依賴分析器jdeps
jdeps是一個很有用的命令行工具。它可以顯示Java類的包級別或類級別的依賴。它接受一個.class文件,一個目錄,或者一個jar文件作為輸入。jdeps默認把結果輸出到系統輸出(控制台)上。
下面我們查看現階段較流行的Spring框架類庫的依賴報告,為了簡化這個例子,我們只分析一個jar文件:org.springframework.core-3.0.5.RELEASE.jar
1
|
jdeps org.springframework.core-3.0.5.RELEASE.jar
|
這個命令輸出的內容很多,所以這里我們只選取一小部分。依賴信息按照包名進行分組。如果依賴不在classpath中,那么就會顯示not found。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
-> java.io
-> java.lang
-> java.lang.annotation
-> java.lang.ref
-> java.lang.reflect
-> java.util
-> java.util.concurrent
-> org.apache.commons.logging not found
-> org.springframework.asm not found
-> org.springframework.asm.commons not found
org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
-> java.lang
-> java.lang.annotation
-> java.lang.reflect
-> java.util
|
更多詳情請參考官方文檔
6. Java虛擬機(JVM)的新特性
PermGen空間被移除了,取而代之的是Metaspace(JEP 122)。JVM選項-XX:PermSize與-XX:MaxPermSize分別被-XX:MetaSpaceSize與-XX:MaxMetaspaceSize所代替。
7. 總結
更多展望:Java 8通過發布一些可以增加程序員生產力的特性來推進這個偉大的平台的進步。現在把生產環境遷移到Java 8還為時尚早,但是在接下來的幾個月里,它會被大眾慢慢的接受。毫無疑問,現在是時候讓你的代碼與Java 8兼容,並且在Java 8足夠安全穩定的時候遷移到Java 8。
作為社區對Java 8的認可,最近Pivotal發布了可在生產環境下支持Java 8的Spring Framework 4.0.3。
如果你喜歡這篇文章,請訂閱我們的郵件列表來查看每周的更新以及免費贈送的白皮書。對於更高級的教程,請查看我們的[JCG學院][JCG]。
我們歡迎你對Java 8中激動人心的特性進行評論!
8. 更多資源
下面一些文章從不同層面上深度討論了Java 8的特性:
- JDK 8 新特性
- Java教程
- WildFly 8, JDK 8, NetBeans 8, Java EE 7
- Java 8教程
- JDK 8命令行工具——靜態依賴分析器
- 探究JDK 8的Javadoc
- Java 8的“黑暗面”
- 在Eclipse Kepler SR2添加Java 8支持
- Java 8
- Oracle Nashorn——JVM平台上的下一代JavaScript引擎
譯文鏈接: http://www.importnew.com/11908.html
[ 轉載請保留原文出處、譯者和譯文鏈接。 ]