本文主要總結了《Java8實戰》,適用于學習 Java8 的同學,也可以作為一個 API 手冊文檔適用,平時使用時可能由于不熟練,忘記 API 或者語法。
接著 Java8使用(上) 繼續補充完剩下的內容。
異步編程
Future
Future 接口在 Java5 中被引入,設計初衷是對將來某個時刻會發生的結果進行建模。它建模了一種異步計算,返回一個執行運算結果的引用,當運算結束后,這個引用被返回給調用方。在 Future 中觸發那些潛在耗時的操作把調用線程解放出來,讓它能繼續執行其他有價值的工作,不再需要呆呆等待耗時的操作完成。
ExecutorService executor = Executors.newCachedThreadPool();
Future < Double > future = executor.submit(new Callable <Double> () {
public Double call() {
return doSomeLongComputation();
}
});
doSomethingElse();
try {
Double result = future.get(1, TimeUnit.SECONDS);
} catch (ExecutionException ee) {
// 計算拋出一個異常
} catch (InterruptedException ie) {
// 當前線程在等待過程中被中斷
} catch (TimeoutException te) {
// 在Future對象完成之前超過已過期
}
這種編程方式讓你的線程可以在 ExecutorService 以并發方式調用另一個線程執行耗時操作的同時,去執行一些其他的任務。
局限性:
Future 接口提供了方法來檢測異步計算是否已經結束(使用isDone 方法) ,等待異步操作結束,以及獲取計算的結果。
- 將兩個異步計算合并為一個——這兩個異步計算之間相互獨立,同時第二個又依賴于第一個的結果。
- 等待 Future 集合中的所有任務都完成。
- 僅等待 Future 集合中最快結束的任務完成(有可能因為它們試圖通過不同的方式計算同一個值) ,并返回它的結果。
- 通過編程方式完成一個 Future 任務的執行(即以手工設定異步操作結果的方式) 。
- 應對 Future 的完成事件(即當 Future 的完成事件發生時會收到通知,并能使用 Future計算的結果進行下一步的操作,不只是簡單地阻塞等待操作的結果) 。
CompletableFuture
CompletableFuture 的 completeExceptionally 方法將導致 CompletableFuture 內發生問題的異常拋出。客戶端現在會收到一個 ExecutionException 異常,該異常接收了一個包含失敗原因的Exception 參數。
使用工廠方法 supplyAsync 創建 CompletableFuture:
public Future <Double> getPriceAsync(String product) {
return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}
supplyAsync 方法接受一個生產者( Supplier )作為參數,返回一個 CompletableFuture對象, 該對象完成異步執行后會讀取調用生產者方法的返回值。 生產者方法會交由 ForkJoinPool池中的某個執行線程( Executor )運行,但是你也可以使用 supplyAsync 方法的重載版本,傳遞第二個參數指定不同的執行線程執行生產者方法。一般而言,向 CompletableFuture 的工廠方法傳遞可選參數,指定生產者方法的執行線程是可行的。
CompletableFuture 和 stream 組合使用:
public List <String> findPrices(String product) {
List < CompletableFuture <String>> priceFutures = shops.stream()
.map(shop -> CompletableFuture.supplyAsync(() - > shop.getName() + " price is " +
shop.getPrice(product)))
.collect(Collectors.toList());
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
利用 CompletableFuture 向其提交任務執行是個不錯的主意。處理需大量使用異步操作的情況時,這幾乎是最有效的策略。
構造同步和異步操作:
public List <String> findPrices(String product) {
List <CompletableFuture <String>> priceFutures =
shops.stream()
.map(shop -> CompletableFuture.supplyAsync(
() -> shop.getPrice(product), executor))
.map(future -> future.thenApply(Quote::parse))
.map(future -> future.thenCompose(quote ->
CompletableFuture.supplyAsync(
() -> Discount.applyDiscount(quote), executor)))
.collect(Collectors.toList());
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
Java8 的 CompletableFuture API 提供了名為 thenCompose 的方法,它就是專門為這一目的而設計的, thenCompose 方法允許你對兩個異步操作進行流水線,第一個操作完成時,將其結果作為參數傳遞給第二個操作。創建兩個 CompletableFuture 對象,對第一個 CompletableFuture 對象調用 thenCompose ,并向其傳遞一個函數。當第一個CompletableFuture 執行完畢后,它的結果將作為該函數的參數,這個函數的返回值是以第一個 CompletableFuture 的返回做輸入計算出的第二個 CompletableFuture 對象。thenCompose 方法像 CompletableFuture 類中的其他方法一樣,也提供了一個以 Async 后綴結尾的版本 thenComposeAsync 。通常而言,名稱中不帶 Async 的方法和它的前一個任務一樣,在同一個線程中運行;而名稱以 Async 結尾的方法會將后續的任務提交到一個線程池,所以每個任務是由不同的線程處理的。
方法名 | 描述 |
---|---|
allOf(CompletableFuture<?>... cfs) | 等待所有任務完成,構造后CompletableFuture完成 |
anyOf(CompletableFuture<?>... cfs) | 只要有一個任務完成,構造后CompletableFuture就完成 |
runAsync(Runnable runnable) | 使用ForkJoinPool.commonPool()作為它的線程池執行異步代碼 |
runAsync(Runnable runnable, Executor executor) | 使用指定的thread pool執行異步代碼 |
supplyAsync(Supplier<U> supplier) | 使用ForkJoinPool.commonPool()作為它的線程池執行異步代碼,異步操作有返回值 |
supplyAsync(Supplier<U> supplier,Executor executor) | 使用指定的thread pool執行異步代碼,異步操作有返回值 |
complete(T t) | 完成異步執行,并返回future的結果 |
completeExceptionlly(Throwable ex) | 異步執行不正常的結束 |
cancel(boolean mayInterruptIfRunning) | 取消任務的執行。參數指定是否立即中斷任務執行,或者等等任務結束 |
isCancelled() | 任務是否已經取消,任務正常完成前將其取消,則返回 true |
isDone() | 任務是否已經完成。需要注意的是如果任務正常終止、異常或取消,都將返回true |
get() | throws InterruptedException, ExecutionException 等待任務執行結束,然后獲得V類型的結果。InterruptedException 線程被中斷異常, ExecutionException任務執行異常,如果任務被取消,還會拋出CancellationException |
get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException | 同上面的get功能一樣,多了設置超時時間。參數timeout指定超時時間,uint指定時間的單位,在枚舉類TimeUnit中有相關的定義。如果計 算超時,將拋出TimeoutException |
thenApply(Function<? super T,? extends U> fn) | 轉換一個新的CompletableFuture對象 |
thenApplyAsync(Function<? super T,? extends U> fn) | 異步轉換一個新的CompletableFuture對象 |
thenApplyAsync(Function<? super T,? extends U> fn, Executor executor) | 使用指定的thread pool執行異步代碼,異步轉換一個新的CompletableFuture對象 |
thenCompose(Function<? super T, ? extends CompletionStage<U>> fn) | 在異步操作完成的時候對異步操作的結果進行一些操作,并且仍然返回CompletableFuture類型 |
thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) | 在異步操作完成的時候對異步操作的結果進行一些操作,并且仍然返回CompletableFuture類型。使用ForkJoinPool |
thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor) | 在異步操作完成的時候對異步操作的結果進行一些操作,并且仍然返回CompletableFuture類型。使用指定的線程池 |
thenAccept(Consumer<? super T> action) | 當CompletableFuture完成計算結果,只對結果執行Action,而不返回新的計算值 |
thenAcceptAsync(Consumer<? super T> action) | 當CompletableFuture完成計算結果,只對結果執行Action,而不返回新的計算值,使用ForkJoinPool |
thenAcceptAsync(Consumer<? super T> action,Executor executor) | 當CompletableFuture完成計算結果,只對結果執行Action,而不返回新的計算值 |
thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn) | 當兩個CompletableFuture都正常完成后,執行提供的fn,用它來組合另外一個CompletableFuture的結果 |
thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn) | 當兩個CompletableFuture都正常完成后,執行提供的fn,用它來組合另外一個CompletableFuture的結果,使用ForkJoinPool |
thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn, Executor executor) | 當兩個CompletableFuture都正常完成后,執行提供的fn,用它來組合另外一個CompletableFuture的結果,使用指定的線程池 |
thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action) | 當兩個CompletableFuture都正常完成后,執行提供的action,用它來組合另外一個CompletableFuture的結果 |
thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action) | 當兩個CompletableFuture都正常完成后,執行提供的action,用它來組合另外一個CompletableFuture的結果,使用ForkJoinPool |
thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action, Executor executor) | 當兩個CompletableFuture都正常完成后,執行提供的action,用它來組合另外一個CompletableFuture的結果,使用指定的線程池 |
whenComplete(BiConsumer<? super T,? super Throwable> action) | 當CompletableFuture完成計算結果時對結果進行處理,或者當CompletableFuture產生異常的時候對異常進行處理 |
whenCompleteAsync(BiConsumer<? super T,? super Throwable> action) | 當CompletableFuture完成計算結果時對結果進行處理,或者當CompletableFuture產生異常的時候對異常進行處理,使用ForkJoinPool |
whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor) | 當CompletableFuture完成計算結果時對結果進行處理,或者當CompletableFuture產生異常的時候對異常進行處理,使用指定的線程池。 |
handle(BiFunction<? super T, Throwable, ? extends U> fn) | 當CompletableFuture完成計算結果或者拋出異常的時候,執行提供的fn |
handleAsync(BiFunction<? super T, Throwable, ? extends U> fn) | 當CompletableFuture完成計算結果或者拋出異常的時候,執行提供的fn,使用ForkJoinPool |
handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor) | 當CompletableFuture完成計算結果或者拋出異常的時候,執行提供的fn,使用指定的線程池 |
- thenApply 的功能相當于將 CompletableFuture<T> 轉換成 CompletableFuture<U>
- thenCompose 可以用于組合多個 CompletableFuture,將前一個結果作為下一個計算的參數,它們之間存在著先后順序
- 現在有 CompletableFuture<T>、CompletableFuture<U> 和一個函數(T,U)->V,thenCompose 就是將 CompletableFuture<T> 和 CompletableFuture<U> 變為 CompletableFuture<V>
- 使用 thenCombine() 之后 future1、future2 之間是并行執行的,最后再將結果匯總。這一點跟 thenCompose() 不同
- thenAcceptBoth 跟 thenCombine 類似,但是返回 CompletableFuture<Void> 類型
- handle() 的參數是 BiFunction,apply() 方法返回 R,相當于轉換的操作
- whenComplete() 的參數是 BiConsumer,accept() 方法返回 void
- thenAccept() 是只會對計算結果進行消費而不會返回任何結果的方法
時間API
Clock
Clock 類提供了訪問當前日期和時間的方法,Clock 是時區敏感的,可以用來取代 System.currentTimeMillis() 來獲取當前的微秒數。某一個特定的時間點也可以使用Instant 類來表示,Instant 類也可以用來創建老的 java.util.Date 對象。
Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant);
LocalDate
該類的實例是一個不可變對象,它只提供了簡單的日期,并不含當天的時間信息。另外,它也不附帶任何與時區相關的信息。通過靜態工廠方法 of 創建一個 LocalDate 實例。 LocalDate 實例提供了多種方法來讀取常用的值,比如年份、月份、星期幾等。
LocalDate date = LocalDate.of(2018, 10, 1);
int year = date.getYear();
Month month = date.getMonth();
int day = date.getDayOfMonth();
DayOfWeek dow = date.getDayOfWeek();
int len = date.lengthOfMonth();
boolean leap = date.isLeapYear();
等同于
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);
獲取當前時間:
LocalDate today = LocalDate.now();
LocalTime
一天中的時間,比如13:45:20,可以使用 LocalTime 類表示。你可以使用 of 重載的兩個工廠方法創建 LocalTime 的實例。 第一個重載函數接收小時和分鐘, 第二個重載函數同時還接收秒。同 LocalDate 一樣, LocalTime 類也提供了一些 getter 方法訪問這些變量的值。
LocalTime time = LocalTime.of(13, 45, 20);
int hour = time.getHour();
int minute = time.getMinute();
int second = time.getSecond();
LocalDate 和 LocalTime 都可以通過解析代表它們的字符串創建。使用靜態方法 parse:
LocalDate date = LocalDate.parse("2018-03-18");
LocalTime time = LocalTime.parse("13:45:20");
可以向 parse 方法傳遞一個 DateTimeFormatter 。該類的實例定義了如何格式化一個日
期或者時間對象。它是替換老版 java.util.DateFormat 的推薦替代品。一旦傳遞的字符串參數無法被解析為合法的 LocalDate 或 LocalTime 對象, 這兩個 parse 方法都會拋出一個繼承自 RuntimeException 的 DateTimeParseException 異常。
LocalDateTime
這個復合類名叫 LocalDateTime ,是 LocalDate 和 LocalTime 的合體。它同時表示了日期和時間, 但不帶有時區信息, 你可以直接創建, 也可以通過合并日期和時間對象構造。
LocalDateTime dt1 = LocalDateTime.of(2018, Month.MARCH, 18, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
通過它們各自的 atTime 或者 atDate 方法,向 LocalDate 傳遞一個時間對象,或者向 LocalTime 傳遞一個日期對象的方式,你可以創建一個 LocalDateTime 對象。你也可以使用toLocalDate 或者 toLocalTime 方法,從 LocalDateTime 中提取 LocalDate 或者 LocalTime組件:
LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();
Instant
可以通過向靜態工廠方法 ofEpochSecond 傳遞一個代表秒數的值創建一個該類的實例。 靜態工廠方法 ofEpochSecond 還有一個增強的重載版本,它接收第二個以納秒為單位的參數值,對傳入作為秒數的參數進行調整。重載的版本會調整納秒參數,確保保存的納秒分片在0到999 999999之間。
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1_000_000_000); // 2秒 之 后 再 加上100萬納秒(1秒)
Instant.ofEpochSecond(4, -1_000_000_000); // 4秒之前的100萬納秒(1秒)
修改操作:
如果你已經有一個 LocalDate 對象, 想要創建它的一個修改版, 最直接也最簡單的方法是使用 withAttribute 方法。 withAttribute 方法會創建對象的一個副本,并按照需要修改它的屬性。
LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
LocalDate date2 = date1.withYear(2011); // 2011-03-18
LocalDate date3 = date2.withDayOfMonth(25); // 2011-03-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9); // 2011-09-25
LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
LocalDate date2 = date1.plusWeeks(1); // 2014-03-25
LocalDate date3 = date2.minusYears(3); // 2011-03-25
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); // 2011-09-25
LocalDate 、 LocalTime 、 LocalDateTime 以及 Instant 通用方法
方法名 | 是否是靜態方法 | 描述 |
---|---|---|
from | 是 | 依據傳入的 Temporal 對象創建對象實例 |
now | 是 | 依據系統時鐘創建 Temporal 對象 |
of | 是 | 由 Temporal 對象的某個部分創建該對象的實例 |
parse | 是 | 由字符串創建 Temporal 對象的實例 |
atOffset | 否 | 將 Temporal 對象和某個時區偏移相結合 |
atZone | 否 | 將 Temporal 對象和某個時區相結合 |
format | 否 | 使用某個指定的格式器將 Temporal 對象轉換為字符串 ( Instant 類不提供該方法) |
get | 否 | 讀取 Temporal 對象的某一部分的值 |
minus | 否 | 創建 Temporal 對象的一個副本, 通過將當前 Temporal 對象的值減去一定的時長創建該副本 |
plus | 否 | 創建 Temporal 對象的一個副本, 通過將當前 Temporal 對象的值加上一定的時長創建該副本 |
with | 否 | 以該 Temporal 對象為模板,對某些狀態進行修改創建該對象的副本 |
LocalDate date = LocalDate.of(2014, 3, 18);
date = date.with(ChronoField.MONTH_OF_YEAR, 9);
date = date.plusYears(2).minusDays(10);
date.withYear(2011);
答案: 2016-09-08 。
每個動作都會創建一個新的 LocalDate 對象,后續的方法調用可以操縱前一方法創建的對象。這段代碼的最后一句不會產生任何我們能看到的效果,因為它像前面的那些操作一樣,會創建一個新的 LocalDate 實例,不過我們并沒有將這個新創建的值賦給任何的變量。
Duration
用于比較 LocalTime 之間的時間差, Duration 類主要用于以秒和納秒衡量時間的長短。
LocalTime time1 = LocalTime.now();
LocalTime time2 = LocalTime.of(11, 0, 0);
Duration d1 = Duration.between(time1, time2);
Period
用于比較 LocalDate 之間的時間差, Period 類主要用于以年月日衡量時間的長短。
Period tenDays = Period.between(LocalDate.of(2014, 3, 8), LocalDate.of(2014, 3, 18));
Duration 和 Period 通用方法
方法名 | 是否是靜態方法 | 方法描述 |
---|---|---|
between | 是 | 創建兩個時間點之間的 interval |
from | 是 | 由一個臨時時間點創建 interval |
of | 是 | 由它的組成部分創建 interval的實例 |
parse | 是 | 由字符串創建 interval 的實例 |
addTo | 否 | 創建該 interval 的副本,并將其疊加到某個指定的 temporal 對象 |
get | 否 | 讀取該 interval 的狀態 |
isNegative | 否 | 檢查該 interval 是否為負值,不包含零 |
isZero | 否 | 檢查該 interval 的時長是否為零 |
minus | 否 | 通過減去一定的時間創建該 interval 的副本 |
multipliedBy | 否 | 將 interval 的值乘以某個標量創建該 interval 的副本 |
negated | 否 | 以忽略某個時長的方式創建該 interval 的副本 |
plus | 否 | 以增加某個指定的時長的方式創建該 interval 的副本 |
subtractFrom | 否 | 從指定的 temporal 對象中減去該 interval |
TemporalAdjuster
將日期調整到下個周日、下個工作日,或者是本月的最后一天。這時,你可以使用重載版本的 with 方法, 向其傳遞一個提供了更多定制化選擇的 TemporalAdjuster 對象,更加靈活地處理日期。
LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
LocalDate date3 = date2.with(TemporalAdjusters.lastDayOfMonth());
方法名 | 描述 |
---|---|
dayOfWeekInMonth | 創建一個新的日期,它的值為同一個月中每一周的第幾天 |
firstDayOfMonth | 創建一個新的日期,它的值為當月的第一天 |
firstDayOfNextMonth | 創建一個新的日期,它的值為下月的第一天 |
firstDayOfNextYear | 創建一個新的日期,它的值為明年的第一天 |
firstDayOfYear | 創建一個新的日期,它的值為當年的第一天 |
firstInMonth | 創建一個新的日期,它的值為同一個月中,第一個符合星期幾要求的值 |
lastDayOfMonth | 創建一個新的日期,它的值為當月的最后一天 |
lastDayOfNextMonth | 創建一個新的日期,它的值為下月的最后一天 |
lastDayOfNextYear | 創建一個新的日期,它的值為明年的最后一天 |
lastDayOfYear | 創建一個新的日期,它的值為今年的最后一天 |
lastInMonth | 創建一個新的日期,它的值為同一個月中,最后一個符合星期幾要求的值 |
next/previous | 創建一個新的日期,并將其值設定為日期調整后或者調整前,第一個符合指定星期幾要求的日期 |
nextOrSame/previousOrSame | 創建一個新的日期,并將其值設定為日期調整后或者調整前,第一個符合指定星期幾要求的日期,如果該日期已經符合要求,直接返回該對象 |
DateTimeFormatter
處理日期和時間對象時,格式化以及解析日期?時間對象是另一個非常重要的功能。新的 java.time.format 包就是特別為這個目的而設計的。這個包中,最重要的類是 DateTime-Formatter。 創建格式器最簡單的方法是通過它的靜態工廠方法以及常量。 像 BASIC_ISO_DATE和 ISO_LOCAL_DATE 這 樣 的 常 量 是 DateTimeFormatter 類 的 預 定 義 實 例 。 所 有 的 DateTimeFormatter 實例都能用于以一定的格式創建代表特定日期或時間的字符串。
LocalDate date = LocalDate.of(2014, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20140318
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2014-03-18
等同于
LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);
和老的 java.util.DateFormat 相比較,所有的 DateTimeFormatter 實例都是線程安全的。所以,你能夠以單例模式創建格式器實例,就像 DateTimeFormatter 所定義的那些常量,并能在多個線程間共享這些實例。 DateTimeFormatter 類還支持一個靜態工廠方法,它可以按照某個特定的模式創建格式器。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date1.format(formatter);
LocalDate date2 = LocalDate.parse(formattedDate, formatter);
如果還需要更加細粒度的控制,DateTimeFormatterBuilder 類還提供了更復雜的格式器,你可以選擇恰當的方法,一步一步地構造自己的格式器。另外,它還提供了非常強大的解析功能,比如區分大小寫的解析、柔性解析(允許解析器使用啟發式的機制去解析輸入,不精確地匹配指定的模式) 、填充,以及在格式器中指定可選節。
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(". ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.YEAR)
.parseCaseInsensitive()
.toFormatter(Locale.ITALIAN);
ZoneId
之前看到的日期和時間的種類都不包含時區信息。時區的處理是新版日期和時間 API 新增加的重要功能,使用新版日期和時間 API 時區的處理被極大地簡化了。新的 java.time.ZoneId 類是老版 java.util.TimeZone 的替代品。它的設計目標就是要讓你無需為時區處理的復雜和繁瑣而操心,比如處理日光時(Daylight Saving Time,DST)這種問題。跟其他日期和時間類一樣, ZoneId 類也是無法修改的。
ZoneId romeZone = ZoneId.of("Europe/Rome");
地區ID都為 “{區域}/{城市}” 的格式, 這些地區集合的設定都由英特網編號分配機構 (IANA)的時區數據庫提供。你可以通過 Java8 的新方法 toZoneId 將一個老的時區對象轉換為 ZoneId :
ZoneId zoneId = TimeZone.getDefault().toZoneId();
一旦得到一個 ZoneId 對象,你就可以將它與 LocalDate 、 LocalDateTime 或者是 Instant 對象整合起來,構造為一個 ZonedDateTime 實例,它代表了相對于指定時區的時間點。
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
ZonedDateTime
將 LocalDateTime 轉換為 Instant :
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
Instant instantFromDateTime = dateTime.toInstant(romeZone);
將 Instant 轉換為 LocalDateTime :
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);
計算時區
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
日歷系統
Java8 中另外還提供了4種其他的日歷系統。這些日歷系統中的每一個都有一個對應的日志類,分別是 ThaiBuddhistDate 、MinguoDate 、 JapaneseDate 以及 HijrahDate 。所有這些類以及 LocalDate 都實現了 ChronoLocalDate 接口,能夠對公歷的日期進行建模。利用 LocalDate 對象,你可以創建這些類的實例。
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
JapaneseDate japaneseDate = JapaneseDate.from(date);
等同于
Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);
ChronoLocalDate now = japaneseChronology.dateNow();
java8類庫
@Repeatable
如果一個注解在設計之初就是可重復的,你可以直接使用它。但是,如果你提供的注解是為用戶提供的,那么就需要做一些工作,說明該注解可以重復。
新增方法
類/接口 | 新方法 |
---|---|
Map | getOrDefault , forEach , compute , computeIfAbsent , computeIfPresent , merge ,putIfAbsent , remove(key,value) , replace , replaceAll |
Iterable | forEach , spliterator |
Iterator | forEachRemaining |
Collection | removeIf , stream , parallelStream |
List | replaceAll , sort |
BitSet | stream |
Map
forEach 該方法簽名為 void forEach(BiConsumer<? super K,? super V> action),作用是對 Map 中的每個映射執行 action 指定的操作,其中 BiConsumer 是一個函數接口,里面有一個待實現方法 void accept(T t, U u)。
java8之前寫法:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
java8:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach((k, v) -> System.out.println(k + "=" + v));
getOrDefault 方法就可以替換現在檢測 Map 中是否包含給定鍵映射的慣用方法。如果 Map 中不存在這樣的鍵映射,你可以提供一個默認值,方法會返回該默認值。
java8 之前寫法:
Map<String, Integer> carInventory = new HashMap<>();
Integer count = 0;
if (map.containsKey("Aston Martin")) {
count = map.get("Aston Martin");
}
java8:
Integer count = map.getOrDefault("Aston Martin", 0);
putIfAbsent 方法簽名為V putIfAbsent(K key, V value),作用是只有在不存在 key 值的映射或映射值為 null 時,才將 value 指定的值放入到 Map 中,否則不對 Map 做更改.該方法將條件判斷和賦值合二為一,使用起來更加方便。
remove(Object key, Object value) 方法,只有在當前 Map 中 key 正好映射到 value 時才刪除該映射,否則什么也不做。
replace 在 Java7 及以前,要想替換 Map 中的映射關系可通過 put(K key, V value) 方法實現,該方法總是會用新值替換原來的值.為了更精確的控制替換行為,Java8 在 Map 中加入了兩個 replace() 方法,分別如下:
- replace(K key, V value),只有在當前 Map 中 key 的映射存在時才用 value 去替換原來的值,否則什么也不做。
- replace(K key, V oldValue, V newValue),只有在當前 Map 中 key 的映射存在且等于 oldValue 時才用 newValue 去替換原來的值,否則什么也不做。
replaceAll 該方法簽名為 replaceAll(BiFunction<? super K,? super V,? extends V> function),作用是對 Map 中的每個映射執行 function 指定的操作,并用 function 的執行結果替換原來的 value,其中 BiFunction 是一個函數接口,里面有一個待實現方法 R apply(T t, U u)。
java8 之前寫法:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
entry.setValue(entry.getValue().toUpperCase());
}
java8:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll((k, v) -> v.toUpperCase());
merge 該方法簽名為 merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction),作用是:
如果 Map 中 key 對應的映射不存在或者為 null,則將 value(不能是 null)關聯到 key 上;
否則執行 remappingFunction,如果執行結果非 null 則用該結果跟 key 關聯,否則在 Map 中刪除 key 的映射。
Map<String, String> myMap = new HashMap<>();
myMap.put("A", "str01A");
myMap.merge("A", "merge01", String::concat); // str01A merge01
compute 該方法簽名為 compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) ,如果 map 里有這個 key,那么 remappingFunction 輸入的 v 就是現在的值,返回的是對應 value,如果沒有這個 key,那么輸入的 v 是 null。
map.compute(key, (k, v) -> v == null ? newMsg : v.concat(newMsg));
computeIfAbsent 該方法簽名為 V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction),作用是:只有在當前 Map 中不存在 key 值的映射或映射值為 null 時,才調用 mappingFunction,并在 mappingFunction 執行結果非 null 時,將結果跟 key 關聯。
java8 之前寫法:
Map<Integer, Set<String>> map = new HashMap<>();
if (map.containsKey(1)) {
map.get(1).add("one");
} else {
Set<String> valueSet = new HashSet<String>();
valueSet.add("one");
map.put(1, valueSet);
}
java8:
map.computeIfAbsent(1, v -> new HashSet<String>()).add("yi");
computeIfPresent 該方法簽名為 V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction),作用跟 computeIfAbsent() 相反,即,只有在當前 Map 中存在 key 值的映射且非 null 時,才調用 remappingFunction,如果 remappingFunction 執行結果為 null,則刪除 key 的映射,否則使用該結果替換 key 原來的映射。
Collection
removeIf 該方法簽名為 boolean removeIf(Predicate<? super E> filter),作用是刪除容器中所有滿足 filter 指定條件的元素,其中 Predicate 是一個函數接口,里面只有一個待實現方法 boolean test(T t),同樣的這個方法的名字根本不重要,因為用的時候不需要書寫這個名字。
java8之前寫法:
// 使用迭代器刪除列表元素
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().length()>3) { // 刪除長度大于3的元素
it.remove();
}
}
java8:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
// 刪除長度大于3的元素
list.removeIf(str -> str.length() > 3);
replaceAll 該方法簽名為 void replaceAll(UnaryOperator<E> operator),作用是對每個元素執行 operator 指定的操作,并用操作結果來替換原來的元素。其中 UnaryOperator 是一個函數接口,里面只有一個待實現函數 T apply(T t)。
java8 之前寫法:
// 使用下標實現元素替換
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for (int i=0; i<list.size(); i++) {
String str = list.get(i);
if (str.length()>3) {
list.set(i, str.toUpperCase());
}
}
java8:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(str -> {
if (str.length() > 3) {
return str.toUpperCase();
}
return str;
});
sort 該方法定義在List接口中,方法簽名為 void sort(Comparator<? super E> c),該方法根據c指定的比較規則對容器元素進行排序。Comparator 接口我們并不陌生,其中有一個方法int compare(T o1, T o2) 需要實現,顯然該接口是個函數接口。
java8 之前寫法:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Collections.sort(list, new Comparator<String>() {
@Override public int compare(String str1, String str2) {
return str1.length() - str2.length();
}
});
java8:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.sort((str1, str2) -> str1.length() - str2.length());
spliterator 方法簽名為 Spliterator<E> spliterator(),該方法返回容器的可拆分迭代器。從名字來看該方法跟 iterator() 方法有點像,我們知道Iterator是用來迭代容器的, Spliterator 也有類似作用,但二者有如下不同:
- Spliterator 既可以像 Iterator 那樣逐個迭代,也可以批量迭代。批量迭代可以降低迭代的開銷。
- Spliterator 是可拆分的,一個 Spliterator 可以通過調用 Spliterator<T> trySplit() 方法來嘗試分成兩個。一個是 this,另一個是新返回的那個,這兩個迭代器代表的元素沒有重疊。
可通過(多次)調用 Spliterator.trySplit() 方法來分解負載,以便多線程處理。
stream 和 parallelStream 分別返回該容器的 Stream 視圖表示,不同之處在于parallelStream() 返回并行的 Stream。Stream 是 Java 函數式編程的核心類。
并發包
原子操作
java.util.concurrent.atomic 包提供了多個對數字類型進行操作的類,比如 AtomicInteger 和 AtomicLong ,它們支持對單一變量的原子操作。這些類在 Java8 中新增了更多的方法支持。
- getAndUpdate —— 以原子方式用給定的方法更新當前值,并返回變更之前的值。
- updateAndGet —— 以原子方式用給定的方法更新當前值,并返回變更之后的值。
- getAndAccumulate —— 以原子方式用給定的方法對當前及給定的值進行更新,并返回變更之前的值。
- accumulateAndGet —— 以原子方式用給定的方法對當前及給定的值進行更新,并返回變更之后的值。
Adder 和 Accumulator:
多線程的環境中,如果多個線程需要頻繁地進行更新操作,且很少有讀取的動作(比如,在統計計算的上下文中) ,Java API 文檔中推薦大家使用新的類 LongAdder 、 LongAccumulator 、DoubleAdder 以及 DoubleAccumulator ,盡量避免使用它們對應的原子類型。這些新的類在設計之初就考慮了動態增長的需求,可以有效地減少線程間的競爭。
LongAddr 和 DoubleAdder 類都支持加法操作,而 LongAccumulator 和 DoubleAccumulator 可以使用給定的方法整合多個值。
LongAdder adder = new LongAdder();
adder.add(10);
long sum = adder.sum();
等同于
LongAccumulator acc = new LongAccumulator(Long::sum, 0);
acc.accumulate(10);
long result = acc.get();
ConcurrentHashMap
ConcurrentHashMap 類的引入極大地提升了 HashMap 現代化的程度,新引入的ConcurrentHashMap 對并發的支持非常友好。 ConcurrentHashMap 允許并發地進行新增和更新操作,因為它僅對內部數據結構的某些部分上鎖。因此,和另一種選擇,即同步式的 Hashtable 比較起來,它具有更高的讀寫性能。
- 性能
為了改善性能,要對 ConcurrentHashMap 的內部數據結構進行調整。典型情況下, map 的條目會被存儲在桶中,依據鍵生成哈希值進行訪問。但是,如果大量鍵返回相同的哈希值,由于桶是由 List 實現的,它的查詢復雜度為O(n),這種情況下性能會惡化。在 Java8 中,當桶過于臃腫時,它們會被動態地替換為排序樹(sorted tree) ,新的數據結構具有更好的查詢性能(排序樹的查詢復雜度為O(log(n))) 。注意,這種優化只有當鍵是可以比較的(比如 String 或者 Number類)時才可能發生。 - 類流操作
ConcurrentHashMap 支持三種新的操作,這些操作和你之前在流中所見的很像:
- forEach ——對每個鍵值對進行特定的操作
- reduce ——使用給定的精簡函數(reduction function) ,將所有的鍵值對整合出一個結果
- search ——對每一個鍵值對執行一個函數,直到函數的返回值為一個非空值
以上每一種操作都支持四種形式,接受使用鍵、值、 Map.Entry 以及鍵值對的函數: - 使用鍵和值的操作( forEach 、 reduce 、 search )
- 使用鍵的操作( forEachKey 、 reduceKeys 、 searchKeys )
- 使用值的操作 ( forEachValue 、 reduceValues 、 searchValues )
- 使用 Map.Entry 對象的操作( forEachEntry 、 reduceEntries 、 searchEntries )
注意,這些操作不會對 ConcurrentHashMap 的狀態上鎖。它們只會在運行過程中對元素進行操作。應用到這些操作上的函數不應該對任何的順序,或者其他對象,抑或在計算過程發生變化的值,有依賴。
除此之外,你需要為這些操作指定一個并發閾值。如果經過預估當前 map 的大小小于設定的閾值,操作會順序執行。使用值 1 開啟基于通用線程池的最大并行。使用值 Long.MAX_VALUE 設定程序以單線程執行操作。
下面這個例子中,我們使用 reduceValues 試圖找出 map 中的最大值:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
Optional<Integer> maxValue = Optional.of(map.reduceValues(1, Integer::max));
注意,對 int 、 long 和 double ,它們的 reduce 操作各有不同(比如 reduceValuesToInt 、reduceKeysToLong 等) 。
- 計數
ConcurrentHashMap 類提供了一個新的方法,名叫 mappingCount ,它以長整型 long 返回 map 中映射的數目。我們應該盡量使用這個新方法,而不是老的 size 方法, size 方法返回的類型為 int 。這是因為映射的數量可能是 int 無法表示的。 - 集合視圖
ConcurrentHashMap 類還提供了一個名為 KeySet 的新方法,該方法以 Set 的形式返回ConcurrentHashMap 的一個視圖(對 map 的修改會反映在該 Set 中,反之亦然) 。你也可以使用新的靜態方法 newKeySet ,由 ConcurrentHashMap 創建一個 Set 。
Arrays
使用 parallelSort
parallelSort 方法會以并發的方式對指定的數組進行排序,你可以使用自然順序,也可以為數組對象定義特別的 Comparator 。
使用 setAll 和 parallelSetAll
setAll 和 parallelSetAll 方法可以以順序的方式也可以用并發的方式,使用提供的函數計算每一個元素的值,對指定數組中的所有元素進行設置。該函數接受元素的索引,返回該索引元素對應的值。由于 parallelSetAll 需要并發執行,所以提供的函數必須沒有任何副作用。
int[] evenNumbers = new int[10];
Arrays.setAll(evenNumbers, i -> i * 2); // 0, 2, 4, 6...
使用 parallelPrefix
parallelPrefix 方法以并發的方式, 用用戶提供的二進制操作符對給定數組中的每個元素進行累積計算。
int[] ones = new int[10];
Arrays.fill(ones, 1);
Arrays.parallelPrefix(ones, (a, b) -> a + b);
Number
Number 類中新增的方法如下。
- Short 、 Integer 、 Long 、 Float 和 Double 類提供了靜態方法 sum 、 min 和 max 。
- Integer 和 Long 類提供了 compareUnsigned 、 divideUnsigned 、 remainderUnsigned 和 toUnsignedLong 方法來處理無符號數。
- Integer 和 Long 類也分別提供了靜態方法 parseUnsignedInt 和 parseUnsignedLong將字符解析為無符號 int 或者 long 類型。
- Byte 和 Short 類提供了 toUnsignedInt 和 toUnsignedLong 方法通過無符號轉換將參數轉化為 int 或 者 long 類型 。 類似地 , Integer 類現在也提供了靜態方法toUnsignedLong 。
- Double 和 Float 類提供了靜態方法 isFinite ,可以檢查參數是否為有限浮點數。
- Boolean 類現在提供了靜態方法 logicalAnd 、 logicalOr 和 logicalXor ,可以在兩個boolean 之間執行 and 、 or 和 xor 操作。
- BigInteger 類提供了 byteValueExact 、 shortValueExact 、 intValueExact 和longValueExact 可以將 BigInteger 類型的值轉換為對應的基礎類型。不過,如果在轉換過程中有信息的丟失,方法會拋出算術異常。
Math
如果 Math 中的方法在操作中出現溢出, Math 類提供了新的方法可以拋出算術異常。支持這一異常的方法包括使用 int 和 long 參數的 addExact 、 subtractExact 、 multipleExact 、incrementExact 、 decrementExact 和 negateExact 。此外, Math 類還新增了一個靜態方法toIntExact , 可以將 long 值轉換為 int 值。 其他的新增內容包括靜態方法 floorMod 、 floorDiv和 nextDown 。
Files
Files 類最引人注目的改變是,你現在可以用文件直接產生流。通過 Files.lines 方法你可以以延遲方式讀取文件的內容,并將其作為一個流。此外,還有一些非常有用的靜態方法可以返回流。
- Files.list —— 生成由指定目錄中所有條目構成的 Stream<Path> 。這個列表不是遞歸包含的。由于流是延遲消費的,處理包含內容非常龐大的目錄時,這個方法非常有用。
- Files.walk —— 和 Files.list 有些類似,它也生成包含給定目錄中所有條目的Stream<Path> 。不過這個列表是遞歸的,你可以設定遞歸的深度。注意,該遍歷是依照深度優先進行的。
- Files.find —— 通過遞歸地遍歷一個目錄找到符合條件的條目,并生成一個Stream<Path> 對象。
Reflection
Relection 接口的另一個變化是新增了可以查詢方法參數信息的API,比如,你現在可以使用新增的 java.lang.reflect.Parameter 類查詢方法參數的名稱和修飾符,這個類被新的java.lang.reflect.Executable 類所引用, 而 java.lang.reflect.Executable 通用函數和構造函數共享的父類。
String
String 類也新增了一個靜態方法,名叫 join 。你大概已經猜出它的功能了,它可以用一個分隔符將多個字符串連接起來。
String authors = String.join(", ", "Raoul", "Mario", "Alan");
PS
泛型
Java類型要么是引用類型(比如 Byte 、 Integer 、 Object 、 List ) ,要么是原始類型(比如 int 、 double 、 byte 、 char ) 。但是泛型(比如 Consumer<T> 中的 T )只能綁定到引用類型。這是由泛型內部的實現方式造成的。因此,在Java里有一個將原始類型轉換為對應的引用類型的機制。這個機制叫作裝箱(boxing)。相反的操作,也就是將引用類型轉換為對應accept 方法的實現Lambda是 Function接口的 apply 方法的實現的原始類型,叫作拆箱(unboxing) 。Java還有一個自動裝箱機制來幫助程序員執行這一任務:裝箱和拆箱操作是自動完成的。
工具類庫
Guava、Apache和lambdaj
廣義歸約( Collectors.reducing)
它需要三個參數。
第一個參數是歸約操作的起始值,也是流中沒有元素時的返回值,所以很顯然對于數值和而言 0 是一個合適的值。
第二個參數就是轉換成一個表示其所含熱量的 int 。
第三個參數是一個 BinaryOperator ,將兩個項目累積成一個同類型的值。
求和:
int totalCalories = menu.stream().collect(Collectors.reducing(0, Dish::getCalories, (i, j) -> i + j));
找出集合中最大值:
Optional <Dish> mostCalorieDish = menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
Collectors 類的靜態工廠方法
工廠方法 | 返回類型 | 描 述 | 使用示例 |
---|---|---|---|
toList | List<T> | 把流中所有項目收集到一個 List | List<Dish> dishes = menuStream.collect(Collectors.toList()); |
toSet | Set<T> | 把流中所有項目收集到一個 Set ,刪除重復項 | Set<Dish> dishes = menuStream.collect(Collectors.toSet()); |
toMap | Map<T, K> | 把流中所有項目收集到一個 Map ,刪除重復項,默認情況下,出現重復數據會報錯 | Map<Long, Dish> dishesMap = menuStream.collect(Collectors.toMap(Dish::getCalories));如有重復數據,可以設置使用哪一個數據 Map<Long, Dish> dishesMap = menuStream.collect(Collectors.toMap(Dish::getCalories, d -> d, (d1, d2) -> d1, LinkedHashMap::new)); |
toCollection | Collection<T> | 把流中所有項目收集到給定的供應源創建的集合 | Collection<Dish> dishes = menuStream.collect(Collectors.toCollection(), ArrayList::new); |
counting | Long | 計算流中元素的個數 | long howManyDishes = menuStream.collect(Collectors.counting()); |
summingInt | Integer | 對流中項目的一個整數屬性求和 | int totalCalories = menuStream.collect(Collectors.summingInt(Dish::getCalories)); |
averagingInt | Integer | 計算流中項目 Integer 屬性的平均值 | int avgCalories = menuStream.collect(Collectors.averagingInt(Dish::getCalories)); |
summarizingInt | IntSummaryStatistics | 收集關于流中項目 Integer 屬性的統計值,例如最大、最小、總和與平均值 | IntSummaryStatistics menuStatistics = menuStream.collect(Collectors.summarizingInt(Dish::getCalories)); |
joining | String | 連接對流中每個項目調用 toString 方法所生成的字符串 | String shortMenu = menuStream.map(Dish::getName).collect(Collectors.joining(", ")); |
maxBy | Optional<T> | 一個包裹了流中按照給定比較器選出的最大元素的 Optional ,或如果流為空則為 Optional.empty() | Optional<Dish> fattest = menuStream.collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))); |
minBy | Optional<T> | 一個包裹了流中按照給定比較器選出的最小元素的 Optional ,或如果流為空則為 Optional.empty() | Optional<Dish> lightest = menuStream.collect(Collectors.minBy(Comparator.comparingInt(Dish::getCalories))); |
reducing | 歸約操作產生的類型 | 從一個作為累加器的初始值開始,利用 BinaryOperator 與流中的元素逐個結合,從而將流歸約為單個值 | int totalCalories = menuStream.collect(Collectors.reducing(0, Dish::getCalories, Integer::sum)); |
collectingAndThen | 轉換函數返回的類型 | 包裹另一個收集器,對其結果應用轉換函數 | int howManyDishes = menuStream.collect(Collectors.collectingAndThen(toList(), List::size)); |
groupingBy | Map<K, List<T>> | 根據項目的一個屬性的值對流中的項目分組組,并將屬性值分組結果 Map 的鍵 | Map<Dish.Type,List<Dish>> dishesByType = menuStream.collect(Collectors.groupingBy(Dish::getType)); |
partitioningBy | Map<Boolean,List<T>> | 根據對流中每個項目應用謂詞的結果來對項目進行分區 | Map<Boolean,List<Dish>> vegetarianDishes = menuStream.collect(Collectors.partitioningBy(Dish::isVegetarian)); |
Optional介紹
Optional<T> 類( java.util.Optional )是一個容器類,代表一個值存在或不存在。
- isPresent() 將在 Optional 包含值的時候返回 true , 否則返回 false 。
- ifPresent(Consumer<T> block) 會在值存在的時候執行給定的代碼塊。
- T get() 會在值存在時返回值,否則拋出一個 NoSuchElement 異常。
- T orElse(T other) 會在值存在時返回值,否則返回一個默認值。
線程個數計算方式
如果線程池中線程的數量過多,最終它們會競爭稀缺的處理器和內存資源,浪費大量的時間在上下文切換上。反之,如果線程的數目過少,正如你的應用所面臨的情況,處理器的一些核可能就無法充分利用。Brian Goetz建議,線程池大小與處理器的利用率之比可以使用下面的公式進行估算:
N threads = N CPU * U CPU * (1 + W/C)
其中:
- N CPU 是處理器的核的數目,可以通過 Runtime.getRuntime().availableProcessors() 得到
- U CPU 是期望的CPU利用率(該值應該介于0和1之間)
- W/C 是等待時間與計算時間的比率
并行——使用 parallelStream 還是 CompletableFutures ?
目前為止, 你已經知道對集合進行并行計算有兩種方式: 要么將其轉化為parallelStream, 利用 map 這樣的操作開展工作,要么枚舉出集合中的每一個元素,創建新的線程,在 CompletableFuture 內對其進行操作。后者提供了更多的靈活性,你可以調整線程池的大小,而這能幫助你確保整體的計算不會因為線程都在等待 I/O 而發生阻塞。
我們對使用這些 API 的建議如下。
如果你進行的是計算密集型的操作,并且沒有 I/O,那么推薦使用 Stream 接口,因為實現簡單,同時效率也可能是最高的(如果所有的線程都是計算密集型的,那就沒有必要創建比處理器核數更多的線程) 。
反之,如果你并行的工作單元還涉及等待I/O的操作(包括網絡連接等待) ,那么使用 CompletableFuture 靈活性更好,你可以像前文討論的那樣,依據等待/計算,或者 W/C 的比率設定需要使用的線程數。這種情況不使用并行流的另一個原因是,處理流的流水線中如果發生 I/O 等待, 流的延遲特性會讓我們很難判斷到底什么時候觸發了等待。
配置并行流使用的線程池
并行流內部使用了默認的 ForkJoinPool,它默認的線程數量就是你的處理器數量 , 這個值是由 Runtime.getRuntime().availableProcessors() 得到的。
但是可以通 過系統屬性 java.util.concurrent.ForkJoinPool.common.parallelism 來改變線程池大小,如下所示:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
這是一個全局設置,因此它將影響代碼中所有的并行流。反過來說,目前還無法專為某個
并行流指定這個值。一般而言,讓 ForkJoinPool 的大小等于處理器數量是個不錯的默認值,
除非你有很好的理由,否則我們強烈建議你不要修改它。
測量流性能
我們聲稱并行求和方法應該比順序和迭代方法性能好。然而在軟件工程上,靠猜絕對不是什么好辦法!特別是在優化性能時,你應該始終遵循三個黃金規則:測量,測量,再測量。
- 并行流并不總是比順序流快。
有些操作本身在并行流上的性能就比順序流差。特別是 limit 和 findFirst 等依賴于元素順序的操作,它們在并行流上執行的代價非常大。例如, findAny 會比 findFirst 性能好,因為它不一定要按順序來執行。你總是可以調用 unordered 方法來把有序流變成無序流。那么,如果你需要流中的n個元素而不是專門要前n個的話,對無序并行流調用 limit 可能會比單個有序流(比如數據源是一個 List)更高效。 - 留意裝箱。自動裝箱和拆箱操作會大大降低性能。Java8 中有原始類型流(IntStream 、LongStream 、 DoubleStream)來避免這種操作,但凡有可能都應該用這些流。
- 考慮流的操作流水線的總計算成本。設N是要處理的元素的總數,Q是一個元素通過流水線的大致處理成本,則N*Q就是這個對成本的一個粗略的定性估計。Q值較高就意味著使用并行流時性能好的可能性比較大。
- 對于較小的數據量,選擇并行流幾乎從來都不是一個好的決定。并行處理少數幾個元素的好處還抵不上并行化造成的額外開銷。
- 考慮流背后的數據結構是否易于分解。例如, ArrayList 的拆分效率比 LinkedList 高得多,因為前者用不著遍歷就可以平均拆分,而后者則必須遍歷。另外,用 range 工廠方法創建的原始類型流也可以快速分解。
可分解性總結了一些流數據源適不適于并行:
源 | 可分解性 |
---|---|
ArrayList | 極佳 |
LinkedList | 差 |
IntStream.range | 極佳 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |