這篇文章最初發佈在我的部落格上。
您還應該閱讀我的 Java 11 教程(包括 Java 9、10 和 11 中的新語言和 API 功能)。
歡迎閱讀我對 Java 8 的介紹。在簡短的程式碼範例的支援下,您將學習如何使用預設介面方法、lambda 表達式、方法引用和可重複註解。在本文末尾,您將熟悉最新的 API 更改,例如串流、功能介面、地圖擴充和新的 Date API。沒有文字牆,只有一堆註解的程式碼片段。享受!
★★★ 喜歡這個項目嗎?留下星星、關注 Twitter 或捐款支持我的工作。謝謝! ★★★
Java 8 讓我們能夠利用default
關鍵字為介面新增非抽象方法實作。此功能也稱為虛擬擴充方法。
這是我們的第一個例子:
interface Formula {
double calculate ( int a );
default double sqrt ( int a ) {
return Math . sqrt ( a );
}
}
除了抽象方法calculate
介面Formula
也定義了預設方法sqrt
。具體類別只需實作抽象方法calculate
。預設方法sqrt
可以直接使用。
Formula formula = new Formula () {
@ Override
public double calculate ( int a ) {
return sqrt ( a * 100 );
}
};
formula . calculate ( 100 ); // 100.0
formula . sqrt ( 16 ); // 4.0
該公式作為匿名物件實作。程式碼相當冗長:6 行程式碼就完成了sqrt(a * 100)
的簡單計算。正如我們將在下一節中看到的,在 Java 8 中實作單一方法物件有一種更好的方法。
讓我們從一個簡單的範例開始,了解如何在早期版本的 Java 中對字串清單進行排序:
List < String > names = Arrays . asList ( "peter" , "anna" , "mike" , "xenia" );
Collections . sort ( names , new Comparator < String >() {
@ Override
public int compare ( String a , String b ) {
return b . compareTo ( a );
}
});
靜態實用方法Collections.sort
接受一個清單和一個比較器,以便對給定清單的元素進行排序。您經常發現自己建立匿名比較器並將它們傳遞給排序方法。
Java 8 沒有整天創建匿名對象,而是提供了更短的語法,即 lambda 表達式:
Collections . sort ( names , ( String a , String b ) -> {
return b . compareTo ( a );
});
正如您所看到的,程式碼更短且更易於閱讀。但它變得更短:
Collections . sort ( names , ( String a , String b ) -> b . compareTo ( a ));
對於一行方法體,您可以跳過大括號{}
和return
關鍵字。但它變得更短:
names . sort (( a , b ) -> b . compareTo ( a ));
列表現在有一個sort
方法。此外,java 編譯器知道參數類型,因此您也可以跳過它們。讓我們更深入地了解如何在野外使用 lambda 表達式。
lambda 表達式如何適應 Java 的型別系統?每個 lambda 對應於由介面指定的給定類型。所謂的函數式介面必須恰好包含一個抽象方法宣告。該類型的每個 lambda 表達式都將與該抽象方法相符。由於預設方法不是抽象的,因此您可以自由地將預設方法新增至功能介面。
我們可以使用任意介面作為 lambda 表達式,只要該介面只包含一個抽象方法即可。為了確保您的介面滿足要求,您應該添加@FunctionalInterface
註釋。當您嘗試在介面中新增第二個抽象方法聲明時,編譯器會識別此註解並拋出編譯器錯誤。
例子:
@ FunctionalInterface
interface Converter < F , T > {
T convert ( F from );
}
Converter < String , Integer > converter = ( from ) -> Integer . valueOf ( from );
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
請記住,如果省略@FunctionalInterface
註釋,程式碼也有效。
透過使用靜態方法引用可以進一步簡化上面的範例程式碼:
Converter < String , Integer > converter = Integer :: valueOf ;
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
Java 8 允許您透過::
關鍵字傳遞方法或建構子的參考。上面的例子展示如何引用靜態方法。但我們也可以引用物件方法:
class Something {
String startsWith ( String s ) {
return String . valueOf ( s . charAt ( 0 ));
}
}
Something something = new Something ();
Converter < String , String > converter = something :: startsWith ;
String converted = converter . convert ( "Java" );
System . out . println ( converted ); // "J"
讓我們看看::
關鍵字如何用於建構函式。首先我們定義一個具有不同建構函數的範例類別:
class Person {
String firstName ;
String lastName ;
Person () {}
Person ( String firstName , String lastName ) {
this . firstName = firstName ;
this . lastName = lastName ;
}
}
接下來我們指定一個用於建立新人員的人員工廠介面:
interface PersonFactory < P extends Person > {
P create ( String firstName , String lastName );
}
我們不是手動實現工廠,而是透過建構函數引用將所有內容黏合在一起:
PersonFactory < Person > personFactory = Person :: new ;
Person person = personFactory . create ( "Peter" , "Parker" );
我們透過Person::new
建立對 Person 建構函數的參考。 Java 編譯器透過符合PersonFactory.create
的簽章自動選擇正確的建構子。
從 lambda 表達式存取外部作用域變數與匿名物件非常相似。您可以從本機外部作用域存取最終變數以及實例欄位和靜態變數。
我們可以從 lambda 表達式的外部作用域讀取最終局部變數:
final int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
但與匿名物件不同的是,變數num
不必宣告為 Final。此程式碼也是有效的:
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
但是num
必須是隱式最終的才能編譯程式碼。以下程式碼無法編譯:
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
num = 3 ;
也禁止從 lambda 表達式內寫入num
。
與局部變數相比,我們可以從 lambda 表達式讀取和寫入實例欄位和靜態變數。這種行為在匿名物件中是眾所周知的。
class Lambda4 {
static int outerStaticNum ;
int outerNum ;
void testScopes () {
Converter < Integer , String > stringConverter1 = ( from ) -> {
outerNum = 23 ;
return String . valueOf ( from );
};
Converter < Integer , String > stringConverter2 = ( from ) -> {
outerStaticNum = 72 ;
return String . valueOf ( from );
};
}
}
還記得第一部分的公式範例嗎?介面Formula
定義了一個預設方法sqrt
,可以從每個公式實例(包括匿名物件)存取該方法。這不適用於 lambda 表達式。
無法從 lambda 表達式內存取預設方法。以下程式碼無法編譯:
Formula formula = ( a ) -> sqrt ( a * 100 );
JDK 1.8 API 包含許多內建的函數介面。其中一些在舊版的 Java 中眾所周知,例如Comparator
或Runnable
。這些現有介面經過擴展,可透過@FunctionalInterface
註釋啟用 Lambda 支援。
但 Java 8 API 也充滿了新的功能接口,讓您的生活更輕鬆。其中一些新介面在 Google Guava 庫中眾所周知。即使您熟悉這個程式庫,您也應該密切注意如何透過一些有用的方法擴充功能來擴展這些介面。
謂詞是一個參數的布林值函數。此介麵包含各種預設方法,用於將謂詞組成複雜的邏輯術語(and、or、negate)
Predicate < String > predicate = ( s ) -> s . length () > 0 ;
predicate . test ( "foo" ); // true
predicate . negate (). test ( "foo" ); // false
Predicate < Boolean > nonNull = Objects :: nonNull ;
Predicate < Boolean > isNull = Objects :: isNull ;
Predicate < String > isEmpty = String :: isEmpty ;
Predicate < String > isNotEmpty = isEmpty . negate ();
函數接受一個參數並產生一個結果。預設方法可用於將多個函數連結在一起(compose、andThen)。
Function < String , Integer > toInteger = Integer :: valueOf ;
Function < String , String > backToString = toInteger . andThen ( String :: valueOf );
backToString . apply ( "123" ); // "123"
供應商產生給定泛型類型的結果。與函數不同,供應商不接受參數。
Supplier < Person > personSupplier = Person :: new ;
personSupplier . get (); // new Person
使用者表示要對單一輸入參數執行的操作。
Consumer < Person > greeter = ( p ) -> System . out . println ( "Hello, " + p . firstName );
greeter . accept ( new Person ( "Luke" , "Skywalker" ));
比較器在舊版的 Java 中就廣為人知。 Java 8 在介面中新增了各種預設方法。
Comparator < Person > comparator = ( p1 , p2 ) -> p1 . firstName . compareTo ( p2 . firstName );
Person p1 = new Person ( "John" , "Doe" );
Person p2 = new Person ( "Alice" , "Wonderland" );
comparator . compare ( p1 , p2 ); // > 0
comparator . reversed (). compare ( p1 , p2 ); // < 0
可選不是函數式接口,而是防止NullPointerException
的漂亮實用程式。這是下一節的一個重要概念,所以讓我們快速了解可選是如何運作的。
可選是一個簡單的值容器,可以為空或非空。考慮一個可能傳回非空結果但有時不傳回任何內容的方法。在 Java 8 中,您傳回的是一個Optional
而不是傳回null
。
Optional < String > optional = Optional . of ( "bam" );
optional . isPresent (); // true
optional . get (); // "bam"
optional . orElse ( "fallback" ); // "bam"
optional . ifPresent (( s ) -> System . out . println ( s . charAt ( 0 ))); // "b"
java.util.Stream
表示可以對其執行一個或多個操作的元素序列。流操作要么是中間操作,要么是最終操作。雖然終端操作會傳回某種類型的結果,但中間操作返回流本身,因此您可以連續連結多個方法呼叫。流是在來源上建立的,例如java.util.Collection
如清單或集合(不支援映射)。流操作可以順序執行,也可以並行執行。
串流非常強大,因此我編寫了單獨的 Java 8 Streams 教程。您還應該查看 Sequency 作為一個類似的網路庫。
讓我們先看看順序流是如何運作的。首先,我們以字串列表的形式建立一個範例來源:
List < String > stringCollection = new ArrayList <>();
stringCollection . add ( "ddd2" );
stringCollection . add ( "aaa2" );
stringCollection . add ( "bbb1" );
stringCollection . add ( "aaa1" );
stringCollection . add ( "bbb3" );
stringCollection . add ( "ccc" );
stringCollection . add ( "bbb2" );
stringCollection . add ( "ddd1" );
Java 8 中的集合得到了擴展,因此您可以透過呼叫Collection.stream()
或Collection.parallelStream()
來簡單地建立流。以下部分解釋了最常見的流程操作。
過濾器接受謂詞來過濾流的所有元素。此操作是中間操作,它使我們能夠對結果呼叫另一個流操作 ( forEach
)。 ForEach 接受要為過濾流中的每個元素執行的使用者。 ForEach 是一個終端操作。它是void
,所以我們不能呼叫另一個流操作。
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa2", "aaa1"
Sorted 是一個中間操作,它傳回流的排序視圖。除非您傳遞自訂Comparator
否則元素會按自然順序排序。
stringCollection
. stream ()
. sorted ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa1", "aaa2"
請記住, sorted
僅建立流的排序視圖,而不操作支援的集合的順序。 stringCollection
的順序保持不變:
System . out . println ( stringCollection );
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
中間操作map
透過給定的函數將每個元素轉換為另一個物件。以下範例將每個字串轉換為大寫字串。但您也可以使用map
將每個物件轉換為另一種類型。結果流的泛型類型取決於傳遞給map
函數的泛型類型。
stringCollection
. stream ()
. map ( String :: toUpperCase )
. sorted (( a , b ) -> b . compareTo ( a ))
. forEach ( System . out :: println );
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
可以使用各種匹配操作來檢查某個謂詞是否與流匹配。所有這些操作都是終端操作並返回布林結果。
boolean anyStartsWithA =
stringCollection
. stream ()
. anyMatch (( s ) -> s . startsWith ( "a" ));
System . out . println ( anyStartsWithA ); // true
boolean allStartsWithA =
stringCollection
. stream ()
. allMatch (( s ) -> s . startsWith ( "a" ));
System . out . println ( allStartsWithA ); // false
boolean noneStartsWithZ =
stringCollection
. stream ()
. noneMatch (( s ) -> s . startsWith ( "z" ));
System . out . println ( noneStartsWithZ ); // true
Count 是一個終端操作,以long
形式傳回流中的元素數量。
long startsWithB =
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "b" ))
. count ();
System . out . println ( startsWithB ); // 3
此終端操作使用給定函數對流的元素執行縮減。結果是一個保留減少值的Optional
值。
Optional < String > reduced =
stringCollection
. stream ()
. sorted ()
. reduce (( s1 , s2 ) -> s1 + "#" + s2 );
reduced . ifPresent ( System . out :: println );
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
如上所述,流可以是順序的或並行的。順序流上的操作在單一執行緒上執行,而並行流上的操作在多個執行緒上並發執行。
以下範例示範了使用並行流來提高效能是多麼容易。
首先,我們建立一個包含獨特元素的大型清單:
int max = 1000000 ;
List < String > values = new ArrayList <>( max );
for ( int i = 0 ; i < max ; i ++) {
UUID uuid = UUID . randomUUID ();
values . add ( uuid . toString ());
}
現在我們測量對該集合的流進行排序所需的時間。
long t0 = System . nanoTime ();
long count = values . stream (). sorted (). count ();
System . out . println ( count );
long t1 = System . nanoTime ();
long millis = TimeUnit . NANOSECONDS . toMillis ( t1 - t0 );
System . out . println ( String . format ( "sequential sort took: %d ms" , millis ));
// sequential sort took: 899 ms
long t0 = System . nanoTime ();
long count = values . parallelStream (). sorted (). count ();
System . out . println ( count );
long t1 = System . nanoTime ();
long millis = TimeUnit . NANOSECONDS . toMillis ( t1 - t0 );
System . out . println ( String . format ( "parallel sort took: %d ms" , millis ));
// parallel sort took: 472 ms
正如您所看到的,兩個程式碼片段幾乎相同,但並行排序速度大約快 50%。您所要做的就是將stream()
更改為parallelStream()
。
正如已經提到的,地圖不直接支援流。 Map
介面本身沒有可用的stream()
方法,但您可以透過map.keySet().stream()
、 map.values().stream()
在映射的鍵、值或在條目上建立專門的流map.entrySet().stream()
。
此外,地圖支援執行常見任務的各種新的有用方法。
Map < Integer , String > map = new HashMap <>();
for ( int i = 0 ; i < 10 ; i ++) {
map . putIfAbsent ( i , "val" + i );
}
map . forEach (( id , val ) -> System . out . println ( val ));
上面的程式碼應該是不言自明的: putIfAbsent
阻止我們編寫額外的 if null 檢查; forEach
接受消費者對映射的每個值執行操作。
此範例展示如何利用函數在地圖上計算程式碼:
map . computeIfPresent ( 3 , ( num , val ) -> val + num );
map . get ( 3 ); // val33
map . computeIfPresent ( 9 , ( num , val ) -> null );
map . containsKey ( 9 ); // false
map . computeIfAbsent ( 23 , num -> "val" + num );
map . containsKey ( 23 ); // true
map . computeIfAbsent ( 3 , num -> "bam" );
map . get ( 3 ); // val33
接下來,我們學習如何刪除給定鍵的條目,前提是它目前映射到給定值:
map . remove ( 3 , "val3" );
map . get ( 3 ); // val33
map . remove ( 3 , "val33" );
map . get ( 3 ); // null
另一個有用的方法:
map . getOrDefault ( 42 , "not found" ); // not found
合併地圖的條目非常簡單:
map . merge ( 9 , "val9" , ( value , newValue ) -> value . concat ( newValue ));
map . get ( 9 ); // val9
map . merge ( 9 , "concat" , ( value , newValue ) -> value . concat ( newValue ));
map . get ( 9 ); // val9concat
如果不存在鍵的條目,則合併將鍵/值放入映射中,否則將呼叫合併函數來變更現有值。
Java 8 在java.time
套件下包含一個全新的日期和時間 API。新的 Date API 與 Joda-Time 庫相當,但並不相同。以下範例涵蓋了這個新 API 的最重要部分。
時鐘提供對當前日期和時間的存取。時脈知道時區,並且可以用來代替System.currentTimeMillis()
來檢索自 Unix EPOCH 以來的當前時間(以毫秒為單位)。時間線上的這樣一個瞬時點也由類Instant
表示。即時可用於建立舊版java.util.Date
物件。
Clock clock = Clock . systemDefaultZone ();
long millis = clock . millis ();
Instant instant = clock . instant ();
Date legacyDate = Date . from ( instant ); // legacy java.util.Date
時區由ZoneId
表示。可以透過靜態工廠方法輕鬆存取它們。時區定義了對於瞬時與本地日期和時間之間的轉換非常重要的偏移量。
System . out . println ( ZoneId . getAvailableZoneIds ());
// prints all available timezone ids
ZoneId zone1 = ZoneId . of ( "Europe/Berlin" );
ZoneId zone2 = ZoneId . of ( "Brazil/East" );
System . out . println ( zone1 . getRules ());
System . out . println ( zone2 . getRules ());
// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]
LocalTime 表示沒有時區的時間,例如晚上 10 點或 17:30:15。以下範例為上面定義的時區建立兩個本地時間。然後我們比較兩個時間併計算兩個時間之間的小時和分鐘差。
LocalTime now1 = LocalTime . now ( zone1 );
LocalTime now2 = LocalTime . now ( zone2 );
System . out . println ( now1 . isBefore ( now2 )); // false
long hoursBetween = ChronoUnit . HOURS . between ( now1 , now2 );
long minutesBetween = ChronoUnit . MINUTES . between ( now1 , now2 );
System . out . println ( hoursBetween ); // -3
System . out . println ( minutesBetween ); // -239
LocalTime 附帶各種工廠方法來簡化新實例的創建,包括解析時間字串。
LocalTime late = LocalTime . of ( 23 , 59 , 59 );
System . out . println ( late ); // 23:59:59
DateTimeFormatter germanFormatter =
DateTimeFormatter
. ofLocalizedTime ( FormatStyle . SHORT )
. withLocale ( Locale . GERMAN );
LocalTime leetTime = LocalTime . parse ( "13:37" , germanFormatter );
System . out . println ( leetTime ); // 13:37
LocalDate 表示不同的日期,例如 2014-03-11。它是不可變的,而且工作原理與 LocalTime 完全相同。此範例示範如何透過增加或減去天、月或年來計算新日期。請記住,每次操作都會傳回一個新實例。
LocalDate today = LocalDate . now ();
LocalDate tomorrow = today . plus ( 1 , ChronoUnit . DAYS );
LocalDate yesterday = tomorrow . minusDays ( 2 );
LocalDate independenceDay = LocalDate . of ( 2014 , Month . JULY , 4 );
DayOfWeek dayOfWeek = independenceDay . getDayOfWeek ();
System . out . println ( dayOfWeek ); // FRIDAY
從字串解析 LocalDate 就像解析 LocalTime 一樣簡單:
DateTimeFormatter germanFormatter =
DateTimeFormatter
. ofLocalizedDate ( FormatStyle . MEDIUM )
. withLocale ( Locale . GERMAN );
LocalDate xmas = LocalDate . parse ( "24.12.2014" , germanFormatter );
System . out . println ( xmas ); // 2014-12-24
LocalDateTime 表示日期時間。它將上面幾節中看到的日期和時間組合到一個實例中。 LocalDateTime
是不可變的,其運作方式與 LocalTime 和 LocalDate 類似。我們可以利用方法從日期時間檢索某些欄位:
LocalDateTime sylvester = LocalDateTime . of ( 2014 , Month . DECEMBER , 31 , 23 , 59 , 59 );
DayOfWeek dayOfWeek = sylvester . getDayOfWeek ();
System . out . println ( dayOfWeek ); // WEDNESDAY
Month month = sylvester . getMonth ();
System . out . println ( month ); // DECEMBER
long minuteOfDay = sylvester . getLong ( ChronoField . MINUTE_OF_DAY );
System . out . println ( minuteOfDay ); // 1439
透過時區的附加訊息,可以將其轉換為即時時間。即時值可以輕鬆轉換為java.util.Date
類型的舊日期。
Instant instant = sylvester
. atZone ( ZoneId . systemDefault ())
. toInstant ();
Date legacyDate = Date . from ( instant );
System . out . println ( legacyDate ); // Wed Dec 31 23:59:59 CET 2014
格式化日期時間就像格式化日期或時間一樣。我們可以從自訂模式建立格式化程序,而不是使用預先定義的格式。
DateTimeFormatter formatter =
DateTimeFormatter
. ofPattern ( "MMM dd, yyyy - HH:mm" );
LocalDateTime parsed = LocalDateTime . parse ( "Nov 03, 2014 - 07:13" , formatter );
String string = formatter . format ( parsed );
System . out . println ( string ); // Nov 03, 2014 - 07:13
與java.text.NumberFormat
不同,新的DateTimeFormatter
是不可變的且線程安全的。
有關模式語法的詳細信息,請閱讀此處。
Java 8 中的註解是可重複的。讓我們直接透過一個例子來弄清楚這一點。
首先,我們定義一個包裝器註釋,其中包含實際註釋的陣列:
@interface Hints {
Hint [] value ();
}
@ Repeatable ( Hints . class )
@interface Hint {
String value ();
}
Java 8 使我們能夠透過宣告註解@Repeatable
來使用相同類型的多個註解。
@ Hints ({ @ Hint ( "hint1" ), @ Hint ( "hint2" )})
class Person {}
@ Hint ( "hint1" )
@ Hint ( "hint2" )
class Person {}
使用變體 2,java 編譯器在底層隱式設定@Hints
註解。這對於透過反射讀取註釋資訊非常重要。
Hint hint = Person . class . getAnnotation ( Hint . class );
System . out . println ( hint ); // null
Hints hints1 = Person . class . getAnnotation ( Hints . class );
System . out . println ( hints1 . value (). length ); // 2
Hint [] hints2 = Person . class . getAnnotationsByType ( Hint . class );
System . out . println ( hints2 . length ); // 2
儘管我們從未在Person
類別上聲明@Hints
註釋,但仍然可以透過getAnnotation(Hints.class)
讀取它。然而,更方便的方法是getAnnotationsByType
,它允許直接存取所有註釋的@Hint
註解。
此外,Java 8 中註解的使用擴展到了兩個新目標:
@ Target ({ ElementType . TYPE_PARAMETER , ElementType . TYPE_USE })
@interface MyAnnotation {}
我的 Java 8 程式指南到此結束。如果您想了解有關 JDK 8 API 的所有新類別和功能的更多信息,請查看我的 JDK8 API Explorer。它可以幫助您找出 JDK 8 的所有新類別和隱藏的精華,例如Arrays.parallelSort
、 StampedLock
和CompletableFuture
- 僅舉幾例。
我還在我的部落格上發表了一系列您可能感興趣的後續文章:
你應該在 Twitter 上關注我。感謝您的閱讀!