Java8 使用(下)

本文主要總結了《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());
}
CompletableFuture 執行流程.png

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

ZonedDateTime.png

將 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 比較起來,它具有更高的讀寫性能。

  1. 性能
    為了改善性能,要對 ConcurrentHashMap 的內部數據結構進行調整。典型情況下, map 的條目會被存儲在桶中,依據鍵生成哈希值進行訪問。但是,如果大量鍵返回相同的哈希值,由于桶是由 List 實現的,它的查詢復雜度為O(n),這種情況下性能會惡化。在 Java8 中,當桶過于臃腫時,它們會被動態地替換為排序樹(sorted tree) ,新的數據結構具有更好的查詢性能(排序樹的查詢復雜度為O(log(n))) 。注意,這種優化只有當鍵是可以比較的(比如 String 或者 Number類)時才可能發生。
  2. 類流操作
    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 等) 。

  1. 計數
    ConcurrentHashMap 類提供了一個新的方法,名叫 mappingCount ,它以長整型 long 返回 map 中映射的數目。我們應該盡量使用這個新方法,而不是老的 size 方法, size 方法返回的類型為 int 。這是因為映射的數量可能是 int 無法表示的。
  2. 集合視圖
    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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容