毫無疑問,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可以由用逗號分隔的參數列表、–>符號與函數體三部分表示。例如:
請注意參數e的類型是由編譯器推測出來的。同時,你也可以通過把參數類型與參數包括在括號中的形式直接給出參數的類型:
在某些情況下lambda的函數體會更加複雜,這時可以把函數體放到在一對花括號中,就像在Java中定義普通函數一樣。例如:
"a""b""d"System.out.print( e );
和:
Lambda可能會返回一個值。返回值的類型也是由編譯器推測出來的。如果lambda的函數體只有一行的話,那麼沒有必要顯式使用return語句。下面兩個代碼片段是等價的:
和:
"a""b""d"int return
public Functional {
method();
}
需要記住的一件事是:默認方法與靜態方法並不影響函數式接口的契約,可以任意使用:
Lambda是Java 8最大的賣點。它具有吸引越來越多程式設計師到Java平臺上的潛力,並且能夠在純Java語言環境中提供一種優雅的方式來支持函數式編程。更多詳情可以參考官方文檔。
2.2 接口的默認方法與靜態方法
Java 8用默認方法與靜態方法這兩個新概念來擴展接口的聲明。默認方法使接口有點像Traits(Scala中特徵(trait)類似於Java中的Interface,但它可以包含實現代碼,也就是目前Java8新增的功能),但與傳統的接口又有些不一樣,它允許在已有的接口中添加新方法,而同時又保持了與舊版本代碼的兼容性。
默認方法與抽象方法不同之處在於抽象方法必須要求實現,但是默認方法則沒有這個要求。相反,每個接口都必須提供一個所謂的默認實現,這樣所有的接口實現者將會默認繼承它(如果有必要的話,可以覆蓋這個默認實現)。讓我們看看下面的例子:
下面的一小段代碼片段把上面的默認方法與靜態方法黏合到一起。
在JVM中,默認方法的實現是非常高效的,並且通過字節碼指令為方法調用提供了支持。默認方法允許繼續使用現有的Java接口,而同時能夠保障正常的編譯過程。這方面好的例子是大量的方法被添加到java.util.Collection接口中去:stream(),parallelStream(),forEach(),removeIf(),……
儘管默認方法非常強大,但是在使用默認方法時我們需要小心注意一個地方:在聲明一個默認方法前,請仔細思考是不是真的有必要使用默認方法,因為默認方法會帶給程序歧義,並且在複雜的繼承體系中容易產生編譯錯誤。更多詳情請參考官方文檔
2.3 方法引用
方法引用提供了非常有用的語法,可以直接引用已有Java類或對象(實例)的方法或構造器。與lambda聯合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗餘代碼。
下面,我們以定義了4個方法的Car這個類作為例子,區分Java中支持的4種不同的方法引用。
第二種方法引用是靜態方法引用,它的語法是Class::static_method。請注意這個方法接受一個Car類型的參數。
第三種方法引用是特定類的任意對象的方法引用,它的語法是Class::method。請注意,這個方法沒有參數。
最後,第四種方法引用是特定對象的方法引用,它的語法是instance::method。請注意,這個方法接受一個Car類型的參數
Car police = Car.create( Car::);
cars.forEach( police::follow );
運行上面的Java程序在控制臺上會有下面的輸出(Car的實例可能不一樣):
正如我們看到的,這裡有個使用@Repeatable( Filters.class )註解的註解類Filter,Filters僅僅是Filter註解的數組,但Java編譯器並不想讓程式設計師意識到Filters的存在。這樣,接口Filterable就擁有了兩次Filter(並沒有提到Filter)註解。
同時,反射相關的API提供了新的函數getAnnotationsByType()來返回重複註解的類型(請注意Filterable.class.getAnnotation( Filters.class )經編譯器處理後將會返回Filters的實例)。
程序輸出結果如下:
null}
T getOrDefault( T value, T defaultValue ) {
( value != ) ? value : defaultValue;
public TypeInference {
staticmain(String[] args) {
Value< String > value = Value<>();
"22"}
}
Value.defaultValue()的參數類型可以被推測出,所以就不必明確給出。在Java 7中,相同的例子將不會通過編譯,正確的書寫方式是 Value.< String >defaultValue()。
2.6 擴展註解的支持
Java 8擴展了註解的上下文。現在幾乎可以為任何東西添加註解:局部變量、泛型類、父類與接口的實現,就連方法的異常也能添加註解。下面演示幾個例子:
com.javacodegeeks.java8.annotations;
java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import
public Annotations {
( RetentionPolicy.RUNTIME )
( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } )
@interface}
staticHolder< T > @NonEmptypublic method() @NonEmpty }
@SuppressWarnings"unused" public void final new Holder< String >();
Collection< String > strings = ArrayList<>();
public ParameterNames {
staticmain(String[] args) Exception {
class"main"classforfinal System.out.println( + parameter.getName() );
}
}
如果不使用–parameters參數來編譯這個類,然後運行這個類,會得到下面的輸出:
如果使用–parameters參數來編譯這個類,程序的結構會有所不同(參數的真實名字將會顯示出來):
對於有經驗的Maven用戶,通過maven-compiler-plugin的配置可以將-parameters參數添加到編譯器中去。
plugin<>org.apache.maven.plugins</>
artifactIdartifactId<>3.1</>
configuration<>-parameters</>
sourcesource<>1.8</>
configurationplugin
);
System.out.println( + fullName.isPresent() );
System.out.println( + fullName.orElseGet( () -> ) );
System.out.println( fullName.map( s -> + s + ).orElse( ) );
如果Optional類的實例為非空值的話,isPresent()返回true,否從返回false。為了防止Optional為空值,orElseGet()方法通過回調函數來產生一個默認值。map()函數對當前Optional的值進行轉化,然後返回一個新的Optional實例。orElse()方法和orElseGet()方法類似,但是orElse接受一個默認值而不是一個回調函數。下面是這個程序的輸出:
更多詳情請參考官方文檔
4.2 Stream
最新添加的Stream API(java.util.stream) 把真正的函數式編程風格引入到Java中。這是目前為止對Java類庫最好的補充,因為Stream API可以極大提供Java程式設計師的生產力,讓程式設計師寫出高效率、乾淨、簡潔的代碼。
Stream API極大簡化了集合框架的處理(但它的處理的範圍不僅僅限於集合框架的處理,這點後面我們會看到)。讓我們以一個簡單的Task類為例進行介紹:
classprivate Status {
};
staticclassprivate Status status;
final
finalfinalthisthis}
Integer getPoints() {
points;
public return }
public return "[%s, %d]"}
Task( Status.OPEN, ),
Task( Status.OPEN, ),
Task( Status.CLOSED, )
);
我們下面要討論的第一個問題是所有狀態為OPEN的任務一共有多少分數?在Java 8以前,一般的解決方式用foreach循環,但是在Java 8裡面我們可以使用stream:一串支持連續、並行聚集操作的元素。
long .stream()
.mapToInt( Task::getPoints )
"Total points: "
totalPoints = tasks
.parallel()
// or map( Task::getPoints )
0"Total points (all tasks): "
這個例子的控制臺輸出如下:
讓我們來計算整個集合中每個task分數(或權重)的平均值來結束task的例子。
Collection< String > result = tasks
// Stream< String >
// IntStream
// LongStream
// DoubleStream
// Stream< Double >
long100// LongStream
"%"// Stream< String>
// List< String >
System.out.println( result );
下面是這個例子的控制臺輸出:
最後,就像前面提到的,Stream API不僅僅處理Java集合框架。像從文本文件中逐行讀取數據這樣典型的I/O操作也很適合用Stream API來處理。下面用一個例子來應證這一點。
Path path = File( filename ).toPath();
trylines.onClose( () -> System.out.println() ).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()。
Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );
下面是程序在控制臺上的輸出:
LocalTime time = LocalTime.now();
final
2014-04-12
11:25:54.568
15:25:54.568
LocaleDateTime把LocaleDate與LocaleTime的功能合併起來,它持有的是ISO-8601格式無時區信息的日期與時間。下面是一個快速入門的例子。
LocalDateTime datetime = LocalDateTime.now();
final
2014-04-12T15:37:52.309
如果你需要特定時區的日期/時間,那麼ZonedDateTime是你的選擇。它持有ISO-8601格式具具有時區信息的日期與時間。下面是一些不同時區的例子:
Get the zoned /time
final ZonedDateTime zonedDatetime = ZonedDateTime.now();
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock );
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( ) );
System.out.println( zonedDatetime );
System.out.println( zonedDatetimeFromClock );
System.out.println( zonedDatetimeFromZone );
下面是程序在控制臺上的輸出:
最後,讓我們看一下Duration類:在秒與納秒級別上的一段時間。Duration使計算兩個日期間的不同變的十分簡單。下面讓我們看一個這方面的例子。
上面的例子計算了兩個日期2014年4月16號與2014年4月16號之間的過程。下面是程序在控制臺上的輸出:
inin
我們在後面的Java新工具章節會再次談到Nashorn。
4.5 Base64
在Java 8中,Base64編碼已經成為Java類庫的標準。它的使用十分簡單,下面讓我們看一個例子:
com.javacodegeeks.java8.base64;
java.nio.charset.StandardCharsets;
import
public Base64s {
staticmain(String[] args) {
String text = ;
String encoded = Base64
.encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
final new Base64.getDecoder().decode( encoded ),
System.out.println( decoded );
Base64 finally Java 8!
Base64類同時還提供了對URL、MIME友好的編碼器與解碼器(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。
4.6 並行(parallel)數組
Java 8增加了大量的新方法來對數組進行並行處理。可以說,最重要的是parallelSort()方法,因為它可以在多核機器上極大提高數組排序的速度。下面的例子展示了新方法(parallelXxx)的使用。
上面的代碼片段使用了parallelSetAll()方法來對一個有20000個元素的數組進行隨機賦值。然後,調用parallelSort方法。這個程序首先列印出前10個元素的值,之後對整個數組排序。這個程序在控制臺上的輸出如下(請注意數組元素是隨機生產的):1
1;
};
-> org.apache.commons.logging not found
-> org.springframework.asm.commons not found
-> java.lang
-> java.lang.reflect
<code plain"="" style="border: 0px !important; font-family: Consolas, "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; border-radius: 0px !important; bottom: auto !important; float: none !important; height: auto !important; left: auto !important; line-height: 1.1em !important; outline: 0px !important; overflow: visible !important; position: static !important; right: auto !important; top: auto !important; vertical-align: baseline !important; width: auto !important; box-sizing: content-box !important; min-height: auto !important; background: none !important;">-> java.util
更多詳情請參考官方文檔
6. Java虛擬機(JVM)的新特性
PermGen空間被移除了,取而代之的是Metaspace(JEP 122)。JVM選項-XX:PermSize與-XX:MaxPermSize分別被-XX:MetaSpaceSize與-XX:MaxMetaspaceSize所代替。