Java8新特性總覽

cfycyf 7年前發布 | 17K 次閱讀 Java8 Java開發

本文主要介紹 Java 8 的新特性,包括 Lambda 表達式、方法引用、流(Stream API)、默認方法、Optional、組合式異步編程、新的時間 API,等等各個方面。

寫在前面

  • 本文是《Java 8 in Action》的讀書筆記,主要提煉了概念性的知識/觀點性的結論,對推導和闡釋沒有摘錄
  • 文中涉及到的源碼請參考我在 GitHub 上的項目 java-learning (地址為 https://github.com/brianway/java-learning )的 Java 8 模塊部分,比書中參考源碼分類更清晰

基礎知識

Java 8 的主要想法:

  • stream API
  • 向方法傳遞代碼的技巧(方法引用、Lambda)
  • 接口中的默認方法

三個編程概念:

  • 流處理(好處:更高抽象,免費并行)
  • 行為參數化(通過 API 來傳遞代碼)
  • 并行與共享的可變數據

函數式編程范式的基石:

  • 沒有共享的可變數據
  • 將方法和函數即代碼傳遞給其它方法的能力

Java 8 使用 Stream API 解決了兩個問題:

  • 集合處理時的套路和晦澀
  • 難以利用多核

Collection 主要是為了存儲和訪問數據,而 Stream 則主要用于描述對數據的計算。

通過行為參數化來傳遞代碼

行為參數化:類似于策略設計模式

類 -> 匿名類 -> Lambda 表達式 ,代碼越來越簡潔

Lambda 表達式

Lambda 表達式:簡潔地表示可傳遞的匿名函數的一種方式

重點留意這四個關鍵詞:匿名、函數、傳遞、簡潔

三個部分:

  • 參數列表
  • 箭頭
  • Lambda 主體

Lambda 基本語法,下面兩者之一:

  • (parameters) -> expression
  • (parameters) -> { statements; }

函數式接口:只定義一個 抽象方法 的接口。函數式接口的抽象方法的簽名稱為 函數描述符

Lambda 表達式允許你以內聯的形式為函數式接口的抽象方法提供實現,并把整個表達式作為函數式接口(一個具體實現)的實例。

常用函數式接口有:Predicate, Consumer, Function, Supplier 等等。

Lambda 的類型是從使用 Lambda 的上下文推斷出來的。上下文中 Lambda 表達式需要的類型稱為目標類型。

方法引用

方法引用主要有三類:

  • (1) 指向靜態方法的方法引用
    • Lambda: (args) -> ClassName.staticMethod(args)
    • 方法引用: ClassName::staticMethod
  • (2) 指向任意類型實例方法的方法引用
    • Lambda: (arg0, rest) -> arg0.instanceMethod(rest)
    • 方法引用: ClassName.instanceMethod (arg0 是 ClassName 類型的)
  • (3) 指向現有對象的實例方法的方法引用
    • Lambda: (args) -> expr.instanceMethod(args)
    • 方法引用: expr::intanceMethod

方法引用就是替代那些轉發參數的 Lambda 表達式的語法糖

流(Stream API)

引入的原因:

  • 聲明性方式處理數據集合
  • 透明地并行處理,提高性能

流的定義:從支持數據處理操作的源生成的元素序列

兩個重要特點:

  • 流水線
  • 內部迭代

流與集合:

  • 集合與流的差異就在于什么時候進行計算
    • 集合是內存中的數據結構,包含數據結構中目前所有的值
    • 流的元素則是按需計算/生成
  • 另一個關鍵區別在于遍歷數據的方式
    • 集合使用 Collection 接口,需要用戶去做迭代,稱為外部迭代
    • 流的 Streams 庫使用內部迭代

流操作主要分為兩大類:

  • 中間操作:可以連接起來的流操作
  • 終端操作:關閉流的操作,觸發流水線執行并關閉它

流的使用:

  • 一個數據源(如集合)來執行一個查詢;
  • 一個中間操作鏈,形成一條流的流水線;
  • 一個終端操作,執行流水線,并能生成結果。

流的流水線背后的理念類似于構建器模式。常見的中間操作有 filter , map , limit , sorted , distinct ;常見的終端操作有 forEach , count , collect 。

使用流

  • 篩選
    • 謂詞篩選:filter
    • 篩選互異的元素:distinct
    • 忽略頭幾個元素:limit
    • 截短至指定長度:skip
  • 映射
    • 對流中每個元素應用函數:map
    • 流的扁平化:flatMap
  • 查找和匹配
    • 檢查謂詞是否至少匹配一個元素:anyMatch
    • 檢查謂詞是否匹配所有元素:allMatch/noneMatch
    • 查找元素:findAny
    • 查找第一個元素:findFirst
  • 歸約(折疊): reduce(初值,結合操作)
    • 元素求和
    • 最大值和最小值

anyMatch , allMatch , noneMatch 都用到了短路; distinct , sorted 是有狀態且無界的, skip , limit , reduce 是有狀態且有界的。

原始類型流特化: IntStream , DoubleStream , LongStream ,避免暗含的裝箱成本。

  • 映射到數值流: mapToInt , mapToDouble , mapToLong
  • 轉換回流對象: boxed
  • 默認值: OptionalInt , OptionalDouble , OptionalLong

數值范圍:

  • range : [起始值,結束值)
  • rangeClosed : [起始值,結束值]

構建流

  • 由值創建流: Stream.of , Stream.empty
  • 由數組創建流: Arrays.stream(數組變量)
  • 由文件生成流: Files.lines
  • 由函數生成流:創建無限流,
    • 迭代: Stream.iterate
    • 生成: Stream.generate

用流收集數據

對流調用 collect 方法將對流中的元素觸發歸約操作(由 Collector 來參數化)。

Collectors 實用類提供了許多靜態工廠方法,用來創建常見收集器的實例,主要提供三大功能:

  • 將流元素歸約和匯總為一個值
  • 元素分組
  • 元素分區

歸約和匯總( Collectors 類中的工廠方法):

  • 統計個數: Collectors.counting
  • 查找流中最大值和最小值: Collectors.maxBy , Collectors.minBy
  • 匯總: Collectors.summingInt , Collectors.averagingInt , summarizingInt / IntSummaryStatistics 。還有對應的 long 和 double 類型的函數
  • 連接字符串: joining
  • 廣義的歸約匯總: Collectors.reducing(起始值,映射方法,二元結合) / Collectors.reducing(二元結合) 。 Collectors.reducing 工廠方法是所有上述特殊情況的一般化。

collect vs. reduce ,兩者都是 Stream 接口的方法,區別在于:

  • 語意問題
    • reduce 方法旨在把兩個值結合起來生成一個新值,是不可變的歸約;
    • collect 方法設計就是要改變容器,從而累積要輸出的結果
  • 實際問題
    • 以錯誤的語義使用 reduce 會導致歸約過程不能并行工作

分組和分區

  • 分組: Collectors.groupingBy
    • 多級分組
    • 按子數組收集數據: maxBy
      • 把收集器的結果轉換為另一種結果 collectingAndThen
      • 與 groupingBy 聯合使用的其他收集器例子: summingInt , mapping
  • 分區:是分組的特殊情況,由一個謂詞作為分類函數(分區函數)

收集器接口:Collector,部分源碼如下:

public interface Collector<T, A, R> {
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    Function<A, R> finisher();
    BinaryOperator<A> combiner();
    Set<Characteristics> characteristics();
}

其中 T、A、R 分別是流中元素的類型、用于累積部分結果的對象類型,以及 collect 操作最終結果的類型。

  • 建立新的結果容器: supplier 方法
  • 將元素添加到結果容器: accumulator 方法,累加器是原位更新
  • 對結果容器應用最終轉換: finisher 方法
  • 合并兩個結果容器: combiner 方法
  • 定義收集器的行為: characteristics 方法,Characteristics 包含 UNORDERED , CONCURRENT , IDENTITY_FINISH

前三個方法已經足以對流進行順序歸約,實踐中實現會復雜點,一是因為流的延遲性質,二是理論上可能要進行并行歸約。

Collectors.toList 的源碼實現:

public static <T> Collector<T, ?, List<T>> toList() {
        return new CollectorImpl<>(
            (Supplier<List<T>>) ArrayList::new,
            List::add,
            (left, right) -> { left.addAll(right); return left; },
            CH_ID);
}
// static final Set<Collector.Characteristics> CH_ID = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));

并行流

并行流就是一個把內容分成多個數據塊,并用不同的線程分別處理每個數據塊的流。

關于并行流的幾點說明:

  • 選擇適當的數據結構往往比并行化算法更重要,比如避免拆箱裝箱的開銷,使用便于拆分的方法而非 iterate。
  • 同時,要保證在內核中并行執行工作的時間比在內核之間傳輸數據的時間長。
  • 使用并行流時要注意避免共享可變狀態。
  • 并行流背后使用的基礎架構是 Java 7 中引入的分支/合并框架。

分支/合并框架

分支/合并框架的目的是以遞歸的方式將可以并行的任務拆分成更小的任務,然后將每個子任務的結果合并起來生成整體結果。

  • RecursiveTast<R> 有一個抽象方法 compute,該方法同時定義了:
    • 將任務拆分成子任務的邏輯
    • 無法/不方便再拆分時,生成單個子任務結果的邏輯
  • 對任務調用 fork 方法可以把它排進 ForkJoinPool,同時對左邊和右邊的子任務調用 fork 的效率要比直接對其中一個調用 compute 低,因為可以其中一個子任務可以重用同一線程,減少開銷

工作竊取:用于池中的工作線程之間重新分配和平衡任務。

Spliterator 代表“可分迭代器”,用于遍歷數據源中的元素。可以延遲綁定。

高效 Java 8 編程

重構、測試、調試

  • 改善代碼的可讀性
    • 用 Lambda 表達式取代匿名類
    • 用方法引用重構 Lambda 表達式
    • 用 Stream API 重構命令式的數據處理
  • 增加代碼的靈活性
    • 采用函數接口
      • 有條件的延遲執行
      • 環繞執行

使用 Lambda 重構面向對象的設計模式:

  • 策略模式
    • 一個代表某個算法的接口
    • 一個或多個該接口的具體實現,它們代表的算法的多種實現
    • 一個或多個使用策略對象的客戶
  • 模版方法
    • 傳統:繼承抽象類,實現抽象方法
    • Lambda:添加一個參數,直接插入不同的行為,無需繼承
  • 觀察者模式
    • 執行邏輯較簡單時,可以用 Lambda 表達式代替類
  • 責任鏈模式
  • 工廠模式
    • 傳統:switch case 或者 反射
    • Lambda:創建一個 Map,將名稱映射到對應的構造函數

調試的方法:

  • 查看棧跟蹤:無論 Lambda 表達式還是方法引用,都無法顯示方法名,較難調試
  • 輸出日志: peek 方法,設計初衷就是在流的每個元素恢復運行之前,插入執行一個動作

默認方法

Java 8 中的接口現在支持在聲明方法的同時提供實現,通過以下兩種方式可以完成:

  1. Java 8 允許在接口內聲明 靜態方法
  2. Java 8 引入了一個新功能:默認方法

默認方法的引入就是為了以兼容的方式解決像 Java API 這樣的類庫的演進問題的。它讓類可以自動地繼承接口的一個默認實現。

向接口添加新方法是 二進制兼容 的,即如果不重新編譯該類,即使不實現新的方法,現有類的實現依舊可以運行。默認方法 是一種以 源碼兼容 方式向接口內添加實現的方法。

抽象類和抽象接口的區別:

  • 一個類只能繼承一個抽象類,但一個類可以實現多個接口
  • 一個抽象類可以通過實例變量保存一個通用狀態,而接口不能有實例變量

默認方法的兩種用例:

  • 可選方法:提供默認實現,減少空方法等無效的模版代碼
  • 行為的多繼承
    • 類型的多繼承
    • 利用正交方法的精簡接口
    • 組合接口

如果一個類使用相同的函數簽名從多個地方繼承了方法,解決沖突的三條規則:

  1. 中的方法優先級最高
  2. 若 1 無法判斷,那么子接口的優先級更高,即優先選擇擁有最具體實現的默認方法的接口
  3. 若 2 還無法判斷,那么繼承了多個接口的類必須通過顯示覆蓋和調用期望的方法,顯示地選擇使用哪一個默認方法的實現。

Optional 取代 null

null 的問題:

  • 錯誤之源:NullPointerException 問題
  • 代碼膨脹:各種 null 檢查
  • 自身無意義
  • 破壞了 Java 的哲學: null 指針
  • 在 Java 類型系統上開了個口子:null 不屬于任何類型

java.util.Optional<T> 對可能缺失的值建模,引入的目的并非是要消除每一個 null 引用,而是幫助你更好地設計出普適的 API。

創建 Optional 對象,三個靜態工廠方法:

  • Optional.empty :創建空的 Optional 對象
  • Optional.of :依據非空值創建 Optional 對象,若傳空值會拋 NPE
  • Optianal.ofNullable :創建 Optional 對象,允許傳空值

使用 map 從 Optional 對象提取和轉換值,Optional 的 map 方法:

  • 若 Optional 包含值,將該值作為參數傳遞給 map,對該值進行轉換后包裝成 Optional
  • 若 Optional 為空,什么也不做,即返回 Optional.empty

使用 flatMap 鏈接 Optional 對象:

由于 Optional 的 map 方法會將轉換結果生成 Optional,對于返回值已經為 Optional 的,就會出現 Optional<Optional<T>> 的情況。類比 Stream API 的 flatMap,Optional 的 flapMap 可以將兩層的 Optional 對象轉換為單一的 Optional 對象。

簡單來說,返回值是 T 的,就用 map 方法;返回值是 Optional<T> 的,就用 flatMap 方法。這樣可以使映射完返回的結果均為 Optional<T>

  • 參數為 null 時,會由 Objects.requireNonNull 拋出 NPE;參數為空的 Optional 對象時,返回 Optional.empty
  • 參數非 null/空的 Optional 對象時,map 返回 Optional;flatMap 返回對象本身

原因可以參考這兩個方法的源碼:

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Objects.requireNonNull(mapper.apply(value));
    }
}

另外,Optional 類設計的初衷僅僅是要支持能返回 Optional 對象的方法。設計時并未考慮將其作為類的字段,所以并未實現 Serializable 接口。

默認行為及解引用 Optional 對象:

  • get() : 返回封裝的變量值,或者拋出 NoSuchElementException
  • orElse(T other) : 提供默認值
  • orElseGet(Supplier<? extends T> other) : orElse 方法的延遲調用版
  • orElseThrow(Supplier<> extends X> exceptionSupplier) : 類似 get ,但可以定制希望拋出的異常類型
  • ifPresent(Consumer<? super T>) : 變量存在時可以執行一個方法

CompletableFuture:組合式異步編程

Future 接口有一定的局限性。CompletableFuture 和 Future 的關系就跟 Stream 和 Collection 的關系一樣。

同步 API 與 異步 API

  • 同步 API:調用方需要等待被調用方結束運行,即使兩者是在不同的線程中運行
  • 異步 API:直接返回,被調用方完成之前是將任務交給另一個線程去做,該線程和調用方是異步的,返回方式有如下兩種:
    • 要么通過回調函數
    • 要么由調用方再執行一個“等待,直到計算完成”的方法調用

使用工廠方法 supplyAsync 創建 CompletableFuture 比較方便,該方法會拋出 CompletableFuture 內發生問題的異常。

代碼的阻塞問題的解決方案及如何選擇:

  • 使用并行流對請求進行并行操作:適用于計算密集型的操作,且沒有 I/O ,此時推薦使用 Stream 接口
  • 使用 CompletableFuture 發起異步請求(可以使用定制的執行器):若涉及等待 I/O 的操作,使用 CompletableFuture 靈活性更好

注意,CompletableFuture 類中的 join 方法和 Future 接口中的 get 有相同的含義,join 不拋出檢測異常。另外,需要使用兩個不同的 Stream 流水線而不是同一個,來避免 Stream的延遲特性引起順序執行

構造同步和異步操作:

  • thenApply 方法不會阻塞代碼的執行
  • thenCompose 方法允許你對兩個異步操作進行流水線,第一個操作完成時,將其結果作為參數傳遞給第二個操作
  • thenCombine 方法將兩個完全不相干的 CompletableFuture 對象的結果整合起來

調用 get 或者 join 方法只會造成阻塞,響應 CompletableFuture 的 completion 事件可以實現等所有數據都完備之后再呈現。 thenAccept 方法在每個 CompletableFuture 上注冊一個操作,該操作會在 CompletableFuture 完成執行后使用它的返回值,即 thenAccept 定義了如何處理 CompletableFuture 返回的結果,一旦 CompletableFuture 計算得到結果,它就返回一個 CompletableFuture<Void> 。

新的時間和日期 API

原來的 java.util.Date 類的缺陷:

  • 這個類無法表示日期,只能以毫秒的精度表示時間
  • 易用性差:年份起始 1900 年,月份從 0 起始
  • toString 方法誤導人:其實并不支持時區

相關類同樣缺陷很多:

  • java.util.Calender 類月份依舊從 0 起始
  • 同時存在 java.util.Date 和 java.util.Calender ,徒添困惑
  • 有的特性只在某一個類提供,如 DateFormat 方法
  • DateFormat 不是線程安全的
  • java.util.Date 和 java.util.Calender 都是可變的

一些新的 API( java.time 包)

  • LocalDate : 該類實例是一個 不可變對象 ,只提供簡單的日期, 并不含當天的時間信息 ,也不附帶任何和時區相關的信息
  • LocalTime : 時間(時、分、秒)
  • LocalDateTime : 是 LocalDate 和 LocalTime 的合體,不含時區信息
  • Instant : 機器的日期和時間則使用 java.time.Instant 類對時間建模,以 Unix 元年時間開始所經歷的秒數進行計算
  • Temporal : 上面四個類都實現了該接口,該接口定義了如何讀取和操縱為時間建模的對象的值
  • Duration : 創建兩個 Temporal 對象之間的 duration。 LocalDateTime 和 Instant 是為不同目的設計的,不能混用,且不能傳遞 LocalDate 當參數。
  • Period : 得到兩個 LocalDate 之間的時長

LocalDate , LocalTime , LocalDateTime 三個類的實例創建都有三種工廠方法: of , parse , now

Duration , Period 有很多工廠方法: between , of ,還有 ofArribute 之類的

以上日期-時間對象都是不可修改的,這是為了更好地支持函數式編程,確保線程安全

操縱時間:

  • withArribute 創建一個對象的副本,并按照需要修改它的屬性。更一般地, with 方法。但注意, 該方法并不是修改原值,而是返回一個新的實例 。類似的方法還有 plus , minus 等
  • 使用 TemporalAdjuster 接口: 用于定制化處理日期,函數式接口,只含一個方法 adjustInto
  • TemporalAdjusters : 對應的工具類,有很多自帶的工廠方法。(如果想用 Lamda 表達式定義 TemporalAdjuster 對象,推薦使用 TemporalAdjusters 類的靜態工廠方法 ofDateAdjuster )

打印輸出及解析日期-時間對象:主要是 java.time.format 包,最重要的類是 DateTimeFormatter 類,所有該類的實例都是 線程安全 的,所以可以單例格式創建格式器實例。

處理不同的時區和歷法使用 java.time.ZoneId 類,該類無法修改。

// ZoneDateTime 的組成部分
ZonedDateTime = LocalDateTime + ZoneId
              = (LocalDate + LocalTime) + ZoneId

結語

本文主要對 Java 8 新特性中的 Lambda 表達式、Stream API、流(Stream API)、默認方法、Optional、組合式異步編程、新的時間 API,等方面進行了簡單的介紹和羅列,至于更泛化的概念,譬如函數式編程、Java 語言以外的東西沒有介紹。當然,很多細節和設計思想還需要進一步閱讀官方文檔/源碼,在實戰中去體會和運用。

參考資料

另外附上 lucida 的幾篇譯文:

 

來自:http://brianway.github.io/2017/03/29/javase-java8/

 

 

 本文由用戶 cfycyf 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!