Java8新特性總覽

Java8新特性總覽

標籤: javajava


[TOC]git


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

寫在前面

  • 本文是《Java 8 in Action》的讀書筆記,主要提煉了概念性的知識/觀點性的結論,對推導和闡釋沒有摘錄算法

  • 文中涉及到的源碼請參考我在 GitHub 上的項目 java-learning (地址爲 https://github.com/brianway/java-learning)的 Java 8 模塊部分,比書中參考源碼分類更清晰express

基礎知識

Java 8 的主要想法:編程

  • stream API設計模式

  • 向方法傳遞代碼的技巧(方法引用、Lambda)api

  • 接口中的默認方法數組

三個編程概念:安全

  • 流處理(好處:更高抽象,免費並行)

  • 行爲參數化(經過 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.Datejava.util.Calender,徒添困惑

  • 有的特性只在某一個類提供,如 DateFormat 方法

  • DateFormat 不是線程安全的

  • java.util.Datejava.util.Calender 都是可變的

一些新的 API(java.time 包)

  • LocalDate: 該類實例是一個不可變對象,只提供簡單的日期,並不含當天的時間信息,也不附帶任何和時區相關的信息

  • LocalTime: 時間(時、分、秒)

  • LocalDateTime: 是 LocalDateLocalTime 的合體,不含時區信息

  • Instant: 機器的日期和時間則使用 java.time.Instant 類對時間建模,以 Unix 元年時間開始所經歷的秒數進行計算

  • Temporal: 上面四個類都實現了該接口,該接口定義瞭如何讀取和操縱爲時間建模的對象的值

  • Duration: 建立兩個 Temporal 對象之間的 duration。LocalDateTimeInstant 是爲不一樣目的設計的,不能混用,且不能傳遞 LocalDate 當參數。

  • Period: 獲得兩個 LocalDate 之間的時長

LocalDateLocalTimeLocalDateTime 三個類的實例建立都有三種工廠方法:of,parse,now

DurationPeriod 有不少工廠方法: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 的幾篇譯文:


做者@brianway更多文章:我的網站 | CSDN | oschina

相關文章
相關標籤/搜索