翻譯文章Java 8 Features – The ULTIMATE Guide的第二部分。第一部分在這裡(連結可能會更改)。
5. Java 8函式庫的新特性
Java 8 增加了許多新類別並擴展了現有類,以更好地支援現代並發、函數式程式設計、日期/時間等。5.1. 類別 可選
著名的NullPointerException是迄今為止 Java 應用程式失敗的最常見原因。很早以前,Google 的優秀專案Guava就提出了Optional
一種解決方案NullPointerException
,從而防止程式碼被 null 檢查污染,從而鼓勵編寫更乾淨的程式碼。受 Google 啟發的 Guava 類別Optional
現在是 Java 8 的一部分。 Optional
它只是一個容器:它可以包含一個值或某種類型Т
,或只是 null。它提供了許多有用的方法,因此顯式空檢查不再合理。請參閱官方文件以獲取更詳細的資訊。讓我們來看看兩個使用的小例子Optional
:帶 null 和不帶 null。
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!" ) );
如果實例包含非空值,則 該方法isPresent()
傳回true ,否則傳回false。如果結果包含 null,則該方法包含一個回退機制,接受函數來產生預設值。map ()方法轉換目前值並傳回一個新實例。該方法與 類似,但它採用預設值而不是函數。這是該程式的輸出: Optional
orElseGet()
Optional
Optional
Optional
orElse()
orElseGet()
Full Name is set? false
Full Name: [none]
Hey Stranger!
讓我們快速看一下另一個例子:
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();
結果會是這樣的:
First Name is set? true
First Name: Tom
Hey Tom!
更詳細的資訊請參考官方文件。
5.2. 流
新加入的 Stream API (java.util.stream
) 引入了 Java 中真正的函數式程式設計。它是迄今為止 Java 庫中最全面的補充,使 Java 開發人員的效率顯著提高,並且使他們能夠創建高效、乾淨和簡潔的程式碼。Stream API 讓處理集合變得更加容易(但不限於它們,我們稍後會看到)。我們以一個簡單的類別為例Task
。
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 );
}
}
}
任務有一定的意義(或偽難度),可以是OPEN或CLOSE。讓我們介紹一小部分可以解決的問題。
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 中,答案是流:支援順序和平行聚合操作的元素序列。
// Подсчет общего количества очков всех активных задач с использованием sum()
final long totalPointsOfOpenTasks = tasks
.stream()
.filter( task -> task.getStatus() == Status.OPEN )
.mapToInt( Task::getPoints )
.sum();
System.out.println( "Total points: " + totalPointsOfOpenTasks );
控制台輸出將如下所示:
Total points: 18
讓我們看看這裡發生了什麼。首先,任務集合被轉換為流表示。然後,該操作filter
會過濾掉所有處於CLOSED狀態的任務。在下一步中,該操作使用每個實例的方法mapToInt
將流轉換Task
為流。最後,使用 方法 對所有點進行求和,從而提供最終結果。在繼續下一個範例之前,需要記住一些有關線程的注意事項(更多詳細資訊請參見此處)。操作分為中間操作和最終操作。 中間操作返回一個新流。它們總是很懶惰;當執行諸如 之類的中間操作時,它們實際上並不執行過濾,而是創建一個新流,該新流在完成後包含與給定謂詞匹配的原始流的元素。 有限運算(例如和)可以透過流傳遞以產生結果或副作用。一旦最終操作完成,該流就被視為已使用,並且不能再次使用。幾乎在所有情況下,最終操作都傾向於透過底層資料來源來完成其遍歷。執行緒的另一個有價值的特性是對並行進程的開箱即用的支援。讓我們來看看這個例子,它找到所有問題的分數總和。 Integer
Task::getPoints
Task
sum
stream
filter
forEach
sum
// 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
。這是控制台輸出:
Total points (all tasks): 26.0
通常需要根據特定標準對元素進行分組。該範例演示了線程如何幫助解決此問題。
// Группировка задач по их статусу
final Map<Status, List<Task>> map = tasks
.stream()
.collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );
控制台輸出如下:
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
為了完成問題範例,讓我們根據總分來計算集合中每個問題的總體百分比(或權重):
// Подсчет веса каждой задачи (How процент от общего количества очков)
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 );
控制台輸出將如下所示:
[19%, 50%, 30%]
最後,正如我們之前提到的,Stream API 不僅僅適用於 Java 集合。典型的 I/O 操作(例如逐行讀取文字檔案)非常適合使用流處理。這是一個小例子來證明這一點。
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 );
}
onConsole
在執行緒上呼叫的 方法傳回一個帶有附加私有處理程序的等效執行緒。close()
當在執行緒上呼叫方法時,將呼叫私有處理程序。Stream API 以及lambda和引用方法以及 Java 8 中的預設方法和靜態方法是現代軟體開發範例的答案。更詳細的資訊請參考官方文件。
5.3. 日期/時間 API (JSR 310)
Java 8 透過提供新的日期和時間 API (JSR 310),為日期和時間管理帶來了新的面貌。日期和時間操作是 Java 開發人員最大的痛點之一。java.util.Date
遵循標準並java.util.Calendar
沒有整體上改善這種情況(甚至可能使其更加混亂)。這就是Joda-Time的誕生:Java 的一個出色的日期/時間 API 替代品。Java 8 (JSR 310) 中的新日期/時間 API 深受Joda-Time的影響,並從中汲取了精華。新包java.time
包含日期、時間、日期/時間、時區、持續時間和時間操作的所有類別。API 設計非常重視不變性:不允許更改(從 中學到的慘痛教訓java.util.Calendar
)。如果需要修改,將會傳回對應類別的新實例。讓我們看看主要的類別和它們的使用範例。第一類Clock
,它使用時區提供對當下時刻、日期和時間的存取。Clock
可以用來代替System.currentTimeMillis()
和TimeZone.getDefault()
。
// Получить системное время How смещение UTC
final Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );
控制台輸出範例:
2014-04-12T15:19:29.282Z
1397315969360
我們將關注的其他新類別是LocaleDate
和LocalTime
。LocaleDate
僅包含 ISO-8601 日曆系統中的日期部分,不包含時區。因此,LocalTime
它僅包含時間碼的一部分>。
// получить местную date и время время
final LocalDate date = LocalDate.now();
final LocalDate dateFromClock = LocalDate.now( clock );
System.out.println( date );
System.out.println( dateFromClock );
// получить местную date и время время
final LocalTime time = LocalTime.now();
final LocalTime timeFromClock = LocalTime.now( clock );
System.out.println( time );
System.out.println( timeFromClock );
控制台輸出範例:
2014-04-12
2014-04-12
11:25:54.568
15:25:54.568
LocalDateTime
連接LocaleDate
andLocalTime
包含 ISO-8601 日曆系統中的日期和時間,但不包含時區。下面給出一個簡單的例子。
// Get the local date/time
final LocalDateTime datetime = LocalDateTime.now();
final LocalDateTime datetimeFromClock = LocalDateTime.now( clock );
System.out.println( datetime );
System.out.println( datetimeFromClock );
控制台輸出範例:
2014-04-12T11:37:52.309
2014-04-12T15:37:52.309
如果您需要特定時區的日期/時間,ZonedDateTime
. 它包含 ISO-8601 日曆系統中的日期和時間。以下是不同時區的一些範例。
// Получение даты/времени для временной зоны
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 );
控制台輸出範例:
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
:以秒和奈秒為單位的時間跨度。這使得兩個日期之間的計算變得非常簡單。讓我們看看如何做到這一點:
// Получаем разницу между двумя датами
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() );
上面的範例計算兩個日期April 16, 2014和April 16, 2015之間的持續時間(以天和小時為單位) 。以下是控制台輸出的範例:
Duration in days: 365
Duration in hours: 8783
Java 8 中新日期/時間的整體印象非常非常正面。部分是因為這些變化是基於久經考驗的基礎(Joda-Time),部分是因為這次問題得到了認真的重新考慮,並且開發人員的聲音得到了傾聽。詳細資訊請參考官方文件。
5.4. Nashorn JavaScript 引擎
Java 8 附帶了新的 Nashorn JavaScript 引擎,它允許您在 JVM 上開發和運行某些類型的 JavaScript 應用程式。Nashorn JavaScript 引擎只是 javax.script.ScriptEngine 的另一個實現,它遵循相同的規則集以允許 Java 和 JavaScript 互動。這是一個小例子。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;" ) );
控制台輸出範例:
jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2
5.5. Base64
最後,隨著 Java 8 的發布,對 Base64 編碼的支援進入了 Java 標準函式庫。它非常易於使用,範例示範了這一點。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 );
}
}
程式的控制台輸出顯示編碼和解碼的文字:
QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!
還有 URL 友善的編碼器/解碼器以及 MIME 友善的編碼器/解碼器(Base64.getUrlEncoder()
/ Base64.getUrlDecoder()
、Base64.getMimeEncoder()
/ Base64.getMimeDecoder()
)的類別。
5.6. 平行數組
Java 8 版本新增了許多用於平行數組處理的新方法。也許其中最重要的是parallelSort()
,它可以大大加快多核心機器上的排序速度。下面的小範例示範了新方法系列 ( parallelXxx
) 的實際應用。
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()
用 20,000 個隨機值填滿數組。此後即可應用parallelSort()
。程式列印排序前後的前 10 個元素,以表示陣列確實已排序。範例程式輸出可能如下所示(請注意,陣列的元素是隨機的)。
Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793
5.7. 平行性
在該類別中新增了新方法,java.util.concurrent.ConcurrentHashMap
以支援基於新新增的流物件和 lambda 表達式的聚合操作。此外,該類別中還新增了新方法java.util.concurrent.ForkJoinPool
來支援共享池(也請參閱我們關於 Java 並發性的免費課程)。新增了一個新類別,java.util.concurrent.locks.StampedLock
以提供基於功能的鎖定,具有三種存取模式用於讀取/寫入控制(可以將其視為不太好的鎖定的更好替代方案java.util.concurrent.locks.ReadWriteLock
)。已新增至包中的新類別java.util.concurrent.atomic
:
- 雙累加器
- 雙加法器
- 長累加器
- 長加法器
6. Java運行環境(JVM)的新特性
該區域PermGen
已退役並由Metaspace (JEP 122) 取代。JVM 選項-XX:PermSize
和-XX:MaxPermSize
已分別替換為-XX:MetaSpaceSize
和-XX:MaxMetaspaceSize
。
GO TO FULL VERSION