Java8

Java8 in action

  1. 沒有共享的可變數據,將方法和函數即代碼傳遞給其他方法的能力就是我們平常所說的函數式編程范式的基石。
  2. Collection主要是為了存儲和訪問數據,而Stream則主要用于描述對數據的計算。這里的關鍵點在于,Stream允許并提倡并行處理一個Stream中的元素。

通過行為參數化傳遞代碼

  1. 行為參數化就是可以幫助你處理頻繁變更的需求的一種軟件開發模式。它意味著拿出一個代碼塊,把它準備好卻不去執行它。這個代碼塊以后作為參數傳遞給另一個方法,稍后再去執行它。
  2. 行為參數化,就是一個方法接受多個不同的地為作為參數,并在內部使用它們,完成不同行為的能力。行為參數化可以讓代碼更好地適應不斷變化的要求,減輕未來的工作量。
  3. 傳遞代碼,就是將新行為作為參數傳遞給方法。但在Java8之前這實現起來很啰嗦。為接口聲明許多只用一次的實體類而造成的啰嗦代碼,在Java8之前可以用匿名類來減少。

Lambda表達式

  1. 可以把Lambda表達式理解為簡潔地表示可傳遞的匿名函數的一種方式:
  • 匿名-----它不像普通方法那樣有一個明確的名稱,寫的少而想的多
  • 函數-----我們說它是函數,因為Lambda函數不像方法那樣屬于某個特定的類。但和方法一樣,Lambda有參數列表,函數主體,返回類型,還可能有可以拋出的異常列表。
  • 傳遞------Lambda表達式可以作為參數傳遞給方法或存儲在變量中。
  • 簡潔------無需像匿名類那樣寫很多模板代碼。
  1. 在哪里使用Lambda表達式,在函數式接口上使用Lambda表達式
  2. 函數式接口:就是只定義一個抽象方法的接口。Lambda表達式允許你直接以內聯的形式為函數式接口的抽象方法提供實現,并把整個表達式作為函數式接口的實例。你用匿名內部類也可以完成同樣的事情,只不過比較笨拙:需要提供一個實現,然后再直接內聯將它實例化。
  • 函數式接口的抽象方法的簽名基本上就是Lambda表達式的簽名。我們將這種抽象方法叫作函數描述符。
  1. 把Lambda付諸實踐:環繞執行模式
  • 第一步:記得行為參數化,傳遞行為正是Lambda的拿手好戲
  • 第二步,使用函數式接口來傳遞行為
  • 第三步,執行一個行為 Lambda表達式允許你直接內聯,為函數式接口的抽象方法提供實現,并且將整個表達式作為函數式接口的一個實例。
  • 第四步:傳遞Lambda
  1. 使用函數式接口。 Java8的設計師在java.util.function包中引入了幾個新的函數式接口。
  • Predicate java.util.function.Predict<T> 接口定義了一個名為test的抽象方法,它接受泛型T對象,并返回一個boolean。在你需要表示一個涉及類型T的布爾表達式時,就可以使用這個接口。
  • Consumer java.util.function.Consumer<T> 定義了一個名叫accept的抽象方法,它接受泛型T的對象,沒有返回(void).你如果需要訪問類型T對象,并對其執行某些操作,就可以使用這個接口。
  • Function java.util.function.Function<T,R>接口定義了一個叫作apply的方法,它接受一個泛型T的對象,并返回一個泛型R的對象。如果你需要定義一個Lambda,將輸入對象的信息映射到輸出,就可以使用這個接口。
  • 原始類型特化 上面的三個泛型函數式接口:Predicate<T>, Consumer<T>和Function<T,R>。還有些函數式接口專為某些類型而設計。 Java類型要么是引用類型(如,Byte,Integer,Object,List),要么是原始類型(比如:int,double,byte,char).但是泛型只能綁定到引用類型。 Java8為我們前面所說的函數式接口帶來了一個專門的版本,以便在輸入和輸出都是原始類型時避免自動裝箱的操作。 一般來說,針對專門的輸入參數類型的函數式接口的名稱都要加上對應的原始類型前綴,如DoublePredicate,IntConsumer,LongBinaryOperator,IntFunction等。Function接口還有針對輸出參數類型的變種:ToIntFunction<T>,IntToDoubleFunction等
  1. 類型檢查,類型推斷以及限制
  • 類型檢查 Lambda的類型是從使用Lambda的上下文推斷出來的。上下文(比如,接受它傳遞的方法的參數,或接受它的值的局部變量)中Lambda表達式需要的類型稱為目標類型。
  • 同樣的Lambda,不同的函數式接口。有了目標類型的概念,同一個Lambda表達式就可以與不同的函數式接口聯系起來,只要它們的抽象方法簽名能夠兼容。 -------特殊的void兼容規則 如果一個Lambda的主體是一個語句表達式,它就和一個返回void的函數描述符兼容(當然需要參數列表也兼容)。如,以下兩行都是合法的,盡管List的add方法返回一個boolean,而不是Consumer上下文(T->void)所要求的void:
//Predicate返回一個booean
Predicate<String> p = s->list.add(s)
//Consumer返回一個void
Consumer<String> b = s->list.add(s);
  • 類型推斷 Java編譯器會從上下文(目標類型)推斷出用什么函數式接口來配合Lambda表達式,這意味著它也可以推斷出適合Lambda的簽名,因為函數描述符可以通過目標類型來得到。這樣做的好處在于,編譯器可以了解Lambda表達式的參數類型,這樣就可以在Lambda語法中省去標注參數類型。
  • 使用局部變量。 Lambda表達式也允許使用自由變量(不是參數,而是在外層作用域中定義的變量),就像匿名類一樣。它們被稱作捕獲Lambda. 關于能做這些變量做什么有一些限制。Lambda可以沒有限制地捕獲(也就是在其主體中引用)實例變量和靜態變量。但局部變量必須聲明為final,或事實上是final.換句話說,Lambda表達式只能捕獲指派給它們的局部變量一次。(注:捕獲實例變量可以被看作捕獲最終局部變量this)
  1. 方法引用 方法引用讓你可以重復使用現有的方法定義,并像Lambda一樣傳遞它們。方法引用就是讓你根據已有的方法實現來創建Lambda表達式。可以把方法引用 看作針對僅僅涉及單一方法的Lambda語法糖,因為你表達同樣的事情時要寫的代碼更少了。方法引用主要有三類
  • 指向靜態方法的方法引用
  • 指向任意類型實例方法的方法引用(如String的length方法)
  • 指向現有對象的實例方法的引用
  • 構造函數引用 對于一個現有的構造函數,你可以利用它的名稱和關鍵字new來創建它的一個引用:ClassName:new。 它的功能與指向靜態方法的引用類似。
  • 在需要函數式接口的地方可以使用Lambda表達式。函數式接口就是僅僅定義一個抽象方法的接口。抽象方法的簽名(稱為函數描述符)描述了Lambda表達式的簽名。
  1. 復合Lamda表達式的有用方法。 Java8的好幾個函數式接口都有為方便而設計的方法。具體而言,許多函數式接口,比如用于傳遞Lambda表達式的Comparator,Function和Predicate都提供了允許你進行復合的方法。 在實踐中,這意味著你可以把簡單的Lambda復合成復雜的表達式。如,你可以讓兩個謂詞之間做一個or操作,組合成一個更大的謂詞。而且,你可以讓一個函數的結果成為另一個函數的輸入。你可能會想,函數式接口中怎么可能有更多的方法呢?竅門在于,我們即將介紹的方法都是默認方法,也就是說它們不是抽象方法。
  • 謂詞復合 謂詞接口包括三個方法:negate,and和or,讓重用已有的Predicate來創建更復雜的謂詞。如,你可使用negate方法返回一個Predicate的非。
  • 函數復合 你還可以把Function接口所代表的Lambda表達式復合起來。Funcation接口為此配了andThen和compose兩個默認方法,它們都返回Function的一個實例。andThen方法會返回一個函數,它先對輸入應用一個給定函數,再對輸出應用另一個函數。你也可以類似地使用compose方法,先把給定的函數用作compose的參數里面給的那個函數,然后再把函數本身用于結果。
  1. 判斷一個操作是惰性求值還是及早求值很簡單:只需看它的返回值。如果返回值是Stream,那么是惰性求值,如果返回值是另一個值或為空,那么就是及早求值。使用這些操作的理想方式就是形成一個惰性求值的鏈,最后用一個及早求值的操作返回想要的結果。

小結:

  • Lambda表達式可以理解為一種匿名函數,它沒有名稱,但有參數列表,函數主體,返回類型,可能還有一個可以拋出的異常列表
  • 只有在接受函數式接口的地方才可以使用Lambda表達式
  • Lambda表達式允許你直接內聯,為函數式接口的抽象方法提供實現,并且將整個表達式作為函數式接口的一個實例。
  • 為了避免裝箱操作,對Predicate<T>和Function<T,R>等通用函數式接口的原始類型特化:IntPredicate,IntToLongFunction等。
  • 環繞執行模式(即在方法所必須的代碼中間,你需要執行點什么操作,如資源分配 和清理)可以配合Lambda提高靈活性和可重用性。
  • Lambda表達式所需要代表的類型稱為目標類型

函數式數據處理

引入流

  1. 流是什么 流是Java API的新成員,它允許你以聲明方式處理數據集合
  • Java8中的Stream API可以讓你寫出這樣的代碼:聲明性--更簡潔,更易讀; 可復合--更靈活; 可并行--性能更好
  1. 流簡介 流到底是什么呢?簡短的定義就是“從支持數據處理操作的源生成的元素序列”。讓我們一步步剖析這個定義。
  • 元素序列 像集合一樣,流也提供了一個接口,可以訪問特定元素類型的一組有序值。因為集合是數據結構,主要目的是以特定的時間/空間復雜度存儲和訪問元素(如ArrayList).但流的目的在于表達計算,如filter,sorted和map.集合講的是數據,流講的是計算
  • 源: 流會使用一個提供數據的源,如集合,數組或輸入/輸出資源
  • 數據處理操作 流的數據處理功能支持類似于數據庫的操作,以及函數式編程語言中的常用操作,如filter,map,reduce,find,match,sort等。流操作可以順序執行,也可并行執行。
  1. 流與集合
  • 粗糙地說,集合與流之間的差異就在于什么時候進行計算。集合是一個內存中的數據結構,它包含數據結構中目前所有的值---集合中的每個元素都得先算出來才能添加到集合中。相比之下,流則是在概念上固定的數據結構(你不能添加或刪除元素),其元素則是按需計算的。
  • 只能遍歷一次 和迭代器一樣,流只能遍歷一次。遍歷完之后,我們就說這個流已經被消費掉了。
  • 外部迭代與內部迭代 Streams庫使用內部迭代---它幫你把迭代做了,還把得到的流值存在了某個地方
  1. 流操作 java.util.stream.Stream中的Stream接口定義了許多操作。它們可以分為兩大類。
  • 中間操作 如filter或sorted等中間操作會返回另一個流。這讓多個操作可以連接起來形成一個查詢。
  • 終端操作 終端操作會從流的流水線生成結果。其結果是任何不是流的值,如List,Integer,甚至void
  • 使用流 一般三件事:1--一個數據源來執行一個查詢。 2--一個中間操作鏈,形成一條流的流水線 3--一個終端操作,執行流水線,并能生成結果。

使用流

  1. 篩選和切片
  • 用謂詞篩選 Stream接口支持filter方法。該操作會接受一個謂詞(一個返回boolean的函數)作為參數,并返回一個包括所有符合謂詞的元素的流。
  • 篩選各異的元素 流還支持一個叫作distinct的方法,它會返回一個元素各異(根據元素的上hashcode和equels方法)的流。
  • 截斷流 流支持limit(n)方法,會返回一個不超過給定長度的流。
  • 跳過元素 流還支持skip(n)方法,返回一個扔掉了前n個元素的流。如果流中元素不足n個,則返回一個空流。
  1. 映射 一個非常常見的數據處理套路就是從某些對象中選擇信息。比如在SQL里,你可以從表里選擇一列。Stream API也通過map和flatMap方法提供了類似的工具。
  • 對流中每一個元素應用函數 流支持map方法,它會接受一個函數作為參數。這個函數會被應用到每個元素上,并將映射成一個新的元素
  • 流的扁平化 flatmap方法讓你把一個流中的每個值都換成另一個流,然后把所有的流連接起來成為一個流。
  1. 查找和匹配 Stream API通過allMatch,anyMatch,noneMatch,findFirst和findAny方法提供了這樣的工具。
  • 檢查謂詞是否至少匹配一個元素 anyMatch方法

  • 檢查謂詞是否匹配所有元素 allMatch

  • noneMatch 流中沒有任何元素與給定的謂詞匹配

  • 查找元素 findAny方法將返回當前流中的任意元素。

  • Optional<T>類是一個容器類,代表一個值存在或不存在。Java8的庫設計人員引入了Optional<T>,這樣就不用返回眾所周知容易出問題的null了。看它的幾個方法:

    --ifPresent() 將在Optional包含值的時候返回ture,否則false.
    --ifPresent(Consumer<T> block) 會在值存在的時候執行給定的代碼塊。
    --T get() 會在值存在時返回值,否則拋出一個NoSuchElement異常
    --T orElse(T other)會在值存在時返回值,否則返回一個默認值。

  • 查找第一個元素 findFirst

  • 何時使用findFirst和findAny ,為什么會同時有findFirst和findAny?答案是并行。找到第一個元素在并行上限制更多。如果你不關心返回的元素是哪個,請使用findAny()

  1. 歸約 如何把一個流中的元素組合起來,使用reduce操作來表達更復雜的查詢,如“計算菜單中的總卡路里”或“菜單中卡路里最高的菜是哪一個”。此類查詢需要將流中所有元素反復結合起來,得到一個值,比如一個Integer.這樣的查詢可以被歸類為歸約操作(將流歸約成一個值)。用函數式編程語言的術語來說,這稱為折疊(fold)
  1. 數值流
  • 原始類型流特化。 Java8引入了三個原始類型特化流接口來解決一般裝箱拆箱問題:intStream,DoubleStream和LongStream,分別將流中的元素特化為int,long和double,從而避免了暗含的裝箱成本。每個接口都帶來了進行常用數值歸約的新方法,如sum,max

---1. 映射到數值流,將流轉化為特化版本的常用方法是mapToInt,mapToDouble和mapToLong。這些方法和前面說的map方法的工作方式一樣,只是它們返回的是一個特化流,而不是Stream<T>

---2. 轉換回對象流 有了數值流,你可能會想把它轉換回非特化流。可以用boxed方法

---3. 默認值OptionlInt。 Optional可以用Integer,String等參考類型來參數化。對于三種原始流特化,也分別有一個Optional原始類型特化版本:OptionalInt,OptionalDouble和OptionalLong.

  • 數值范圍 和數字打交道時,有一個常用的東西就是數值范圍,如,假設你想要生成1和100之間的所有數字。Java8引入了兩個可以用于IntStream和LongStream的靜態方法,幫助生成這種范圍:rang和的rangeClosed.這兩個方法都是第一參數接受起始值,第二個參數接受結束值。但range是不包含結束值的,而rangeClosed則包含結束值。
  1. 構建流
  • 由值創建流

    可以使用靜態方法Stream.of 通過顯式值創建一個流。它可以接受任意數量的參數。

  • 由數組創建流

    你可以使用靜態方法Arrays.stream從數組創建一個流。它接受一個數組作為參數。

  • 由文件生成流

    java中用于處理文件等I/O操作的NIO API已更新,以便利用Stream API. java.nio.file.Files中很多靜態方法都會返回一個流。 如Files.lines,它會返回一上由指定文件中的各行構成的字符串流。

  • 由函數生成的流:創建無限流

    Stream API提供了兩個靜態方法來從函數生成流:Stream.iterate和Stream.generate. 這兩個操作可以創建所謂的無限流:不像從固定集合創建的流那樣有固定大小的流。由iterate和generate產生的流會用給定的函數按需創建值,因此可以無窮地計算下去!一般來說,應該使用limit(n)來對這種流加以限制。

    ---迭代 如:Stream.iterate(0,n->n+2)。 生成偶數流。一般來說,在需要依次生成一系列值的時候應該使用iterate.

    ---生成 與iterate方法類似,generate方法也可讓你按需生成一個無限流,但generate不是依次每個新生成的值應用函數的。它接受一個Supplier<T>類型的Lambda提供新的值。如 Stream.generate(Math::random)

用流收集數據

  1. 收集器簡介
  • 收集器用作高級歸約
    收集器非常有用,用它可以簡潔而靈活地定義collect用來生成的集合的標準。一般來說,Collector會元素應用一個轉換函數(很多時候是不體現任何效果的恒等轉換,如toList),并將結果累積到一個數據結構中,從而產生這一過程的最終輸出。
  • 預定義收集器
    預定義收集器也就是那些從Collectors類提供的工廠方法(如groupBy)創建的收集器。它們主要提供三大功能:
    --將流元素歸約和匯總為一個值
    --元素分組
    --元素分區
    
  1. 歸約和匯總
  • 查找流中最大值和最小值 Collectors.maxBy和Colectors.minBy,來計算流中的最大和最小值。這兩個收集器接收一個Comparator參數來比較流中的元素。

  • 匯總 Collecgors類專門為匯總提供了一個工廠方法:Collectors.summingInt。它可接受一個把對象映射為求和所需要int的函數,并返回一個收集器。該收集器傳遞給普通的collect方法后即可執行我們需要的匯總操作。 Collectors.summingLong和Collectors.summingDouble方法的作用完全一樣,可用于求和字段為long和double的情況。 但匯總不僅僅是求和:還有Collectors.averagingInt,連同對應的averagingLong和averaingDouble可以計算數值的平均數。 目前為止,你已經看到了如何使用收集器來給流中的元素計數,找到這些元素數值屬性的最大值和最小值,以及計算其總和和平均值。不過很多時候,你可能想要得到兩個或更多這樣的的結果,而且你希望只需要一次操作就可以完成。在這種情況下,可以使用summarizingInt工廠方法返回的收集器。如:IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingIng(Dish::getCalories));
    這個收集器會把所有這些信息收集到一個叫作IntSummaryStatistics類里,它提供了方便的取值(getter)方法來訪問結果。

  • 連接字符串 joining工廠方法返回的收集器會把流中每一個對象應用toString方法得到所有字符串連接成一個字符串。 joining內部使用了StringBuilder來把生成的字符串逐個追加起來。但該字符串的可讀性并不好,joining工廠方法有一個重載版本可以接受元素分界符joining(",").
    到目前為止,我們已經探討了各種將流歸約互一個值的收集器。下一節,我們會展示為什么所有這種形式的歸約過程,其實都是Collectors.reducing工廠方法提供的更廣義歸約收集器的特殊情況。

  • 廣義的歸約匯總
    事實上,我們已經討論的所有收集器,都是一個可以用reducing工廠方法定義的歸納過程的特殊情況而已。Collectors.reducing工廠方法是所有這些特殊情況的一般化。 它需要三個參數

 第一個參數是歸約操作的起始值,也中流中沒有元素時的返回值,所以很顯然對于數值和而言0是一個合適的值
 
 第二個參數就是一個轉化函數,如將菜肴轉化成一個表示其所含熱量的int
 
  第三個參數是一個BinaryOperator,將兩個項目累加成一個同類型的值。如兩個int的和

同樣,你可以使用下面的單參數形式的reducing來找到熱量最高的菜。如下所示:

    Optional<Dish> mostCalorieDish =menu.stream().collect(reducing((d1,d2)->d1.getCalories()>d2.getCalories()?d1:d2));

可以把單參數reducing工廠方法創建的收集器看作三參數方法的特殊情況,它把流中的第一個項目作為起點,把恒等函數(即一個函數僅僅是返回其輸入參數)作為一個轉換函數。這也意味著,要是把單參數reducing收集器傳遞給空流的collect方法,收集器就沒有起點。它將因此返回一個Optional<Dish>對象

從邏輯上說,歸約操作的工作原理:利用累積函數,把一個初始化為起始值的累加器,和轉換函數應用到流中每個元素上得到的結果不斷迭代合并起來。

  1. 分組

一個常見的數據為操作是根據一個或多個屬性對集合中的項目進行分組。用Collectors.groupingBy工廠方法返回的收集器就可以輕松地完成這項任務。groupBy接收一個分類函數,用它來把流中的元素分成不同的組。把分組函數返回的值作為映射的鍵,把流中所有具有這個分類值的項目的列表作為對應的映射值。值就是包含所有對應類型的列表。

  • 多級分組

要實現多級分組,我們可以使用一個由雙參數版本的Collectors.groupingBy工廠方法創建的收集器,它除了普通的分類函數之外,還可以接受collector類型的第二個參數,那么要時行二級分組的話,我們可以把內層groupBy傳遞給外層groupingBy,并定義一個為流中項目分類的二級標準。二級分組的結果是兩級map.

  • 按子組收集數據
    上面的小節,可以把第二個groupingBy收集器傳遞給外層收集器來實現多級分組。但進一步說,傳遞給第一個groupingBy的第二個收集器可以是任何類型,而不一定是另一個groupingBy. 如,要數一數菜單中每類菜有多少個,可以傳遞counting收集作為groupingBy收集器的第二個參數。
Map<Dish.Type,Long> typesCount = menu.stream().collect(groupingBy(Dish::getType,counting()));

注意普通的單參數groupingBy(f)(其中f是分類函數)實際上是groupingBy(f,toList())的簡便寫法。

---1. 把收集器的結果轉換為另一種類型。 因為分組操作的Map結果中的每個值上包裝的Optional沒什么用,所 你可能想把它們去掉。要做到這一點,或者更一般地來說,把收集器返回的結果轉換為另一種類型,你可以使用Collectors.collectingAndThen工廠方法返回的收集器。

Map<Dish.Type, Dish> mostCaloricByType =
    menu.stream()
.collect(groupingBy(Dish::getType,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),
Optional::get)));

這個工廠方法接受兩個參數---要轉換的收集器以及轉換函數,并返回另一個收集器。這個收集器相當于舊收集器的一個包裝,collect操作的最后一步就是將返回值用轉換函數做一個映射。在這里,被包起來的收集器就是用maxBy建立的那個,而轉換函數Optional::get則把返回的Optional中的值提取出來。這個操作放在這里是安全的,因為reducing收集器永遠不會返回Optional.empty().
把收集器嵌套起來很常見,它們從外層逐層向里有以下幾點:

1.  groupingBy是最外層,根據菜肴的類型把菜單流分組,得到 三個子流
2. groupingBy收集器包裹著collectingAndThen收集器,因此分組操作得到的每個子流都用這第二個收集器做進一步歸約。
3. collectingAndThen收集器又包裹著第三個收集器maxBy
4. 隨后由歸約收集器進行子流的歸約操作,然后包含它的collectingAndThen收集器會對其結果應用Optional:get轉換函數
5. 對三個子流分別執行這一過程并轉換而得到的三個值,也就是各個類型中熱量最高的Dish,將成為groupingBy收集器返回的Map中與各個分類鍵(Dish的類型)相關聯的值。

---2. 與groupingBy聯合使用的其他收集器的例子
通過groupingBy工廠方法的第二個參數傳遞的收集器將會對分到同一組中的所有流元素執行進一步歸約操作。如:你還重用求出所有菜肴熱量總和的收集器,不過這次是對每一組Dish求和:

Map<Dish.Type,Integer> totalCaloriesByType = menu.stream().collect(groupingBy(Dish::getType,summingInt(Dish::getCalories)));

然而常常和groupingBy聯合使用的另一個收集器是mapping方法生成的。這個方法接受兩個參數:一個函數對流中的元素做變換,另一個則將變換的結果對象收集起來。其目的是在累加之前對每個輸入元素應用一個映射函數,這樣就要可以讓接受特定類型元素的收集器適應不同類型的對象。 例子:比方說你想要知道,對于每種類型的Dish,菜單中都有哪些CaloricLevel.我們可以把groupingBy和mapping收集器結合起來,如下:

Map<Dish.Type,Set<CaloricLevel>> caloricLevelsByType =
    menu.stream().collect(
    goupingBy(Dish::getType,mapping( dish->{if (dish.getCalories<=400) return CaloricLevel.DIET; else if(dish.getCalories()<=700) return CaloricLevel.NORMAL; else return CaloricLevel.FAT;},
    toSet())
    )
    );
  1. 分區
    分區是分組的特殊情況:由一個謂詞(返回一個布爾值的函數)作為分類函數,它稱分區函數。分區函數返回一個布爾值,這意味著得到的分組Map的鍵類型是Boolean
  • 分區的優勢
    分區的好處在于保留了分區函數返回true或false的兩套流元素列表。partitioningBy工廠方法還有一個重載版本,可以傳遞第第二個收集器
  1. 收集器接口
    Collector接口包含了一系列方法,為實現具體的歸約操作(即收集器)提供了范本。
  public interface Collector<T,A,R>{
      Supplier<A> suppler();
      BiConsumer<A,T> accumulator();
      Function<A,R> finisher();
      Set<Characteristics> characteristics();
  }

本列表適用以下定義。

T是流中要收集的項目的泛型。
A是累加器的類型,累加器是在收集過程中用于累加部分結果的對象。
R是收集操作得到的對象(通常但并不一定是集合)的類型。

  • 理解Collector接口聲明的方法
  1. 建立新的結果容器:supplier方法

    supplier方法必須返回一個結果為空的Supplier,也就是一個無參數函數,在調用時它會創建一個空的累加器實例,供數據收集過程使用。

  2. 將元素添加到結果容器:accumulator方法

    accumulator方法會返回執行歸約操作的函數。當遍歷到流中第n個元素時,這個函數執行時會有兩個參數:保存歸約結果的累加器(已收集了流中前n-1個項目),還有第n個元素本身。該函數將返回void,因為累加器是原位更新,即函數的執行改變了它的內部狀態以體現遍歷的元素的效果。

  3. 對結果容器應用最終轉換:finisher方法

    在遍歷完流后,finisher方法必須返回在累加過程的最后要調用的一個函數,以便將累加器對象轉換為整個集合操作的最終結果。

    這三個方法已經足以對流進行順序歸約。實踐中的實現細節可能還要復雜一點,一方面是因為流的延遲性質,可能在collect操作之前還需要完成其他中間操作的流水線,另一方面則是理論上可能要進行并行歸約。

  4. 合并兩個結果容器:combiner方法

    四個方法中的最后一個-----combiner方法會返回一個供歸約操作使用的函數,它定義了對流的各個子部分進行并行處理時,各個子部分歸約所得的累加器要如何合并。

    有了這個第四個方法,就可以對流進行并行歸約了。它會用到Java7中引入的分支/合并框架和Spliterator抽象。

  5. characteristics方法

    characteristics會返回一個不可變的Characteristics集合,它定義了收集器的行為----特別是關于流是否可以并行歸約,以及可以使用哪些優化的提示。Characteristics是一個包含三個項目的枚舉:
    ---UNORDRED--歸約結果不受流中項目的遍歷和累積順序的影響
    ---CONCURRENT--accumulator函數可以從多個線程同時調用,且該收集器可以并行歸約流。如果收集器沒有標為UNORDERED,那它僅在用于無序數據源時才可以并行歸約。
    ---IDENTITY_FINISH--這表明完成器方法返回的函數是一個恒等函數,可以跳過。這種情況下,累加器對象將會直接用作歸約過程的最終結果。這也意味著,將累加器A不加檢查地轉換為結果R是安全的。

并行數據處理與性能

  1. 并行流

    Stream接口可以通過收集源調用parallelStream方法來把集合轉換為并行流。并行流就是把一個內容分成多個數據塊,并用不同的線程分別處理每個數據塊的流。

  • 并行流內部使用了默認的ForkJoinPool,它默認的線程數量就是你的處理器數量,這個值是由Runtime.getRunTime().availableProcessors()得到的。但是你可以通過系統屬性 java.util.concurrent.ForkJoinPool.common.parallelism來改變線程池大小。如下:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");

使用正確的數據結構然后使其并行化工作能保證最佳的性能。特別注意原始類型的裝箱和拆箱操作。

  • 高效使用并行流

    一些幫你決定某個特定情況下是否有必要使用并行流的建議:

    1. 有疑問,測量。把順序流轉化成并行流輕而易舉,但卻不一定是好事。并行流并不總是比順序流快。
    2. 留意裝箱。自動裝箱和拆箱操作會大大降低性能。Java8中有原始類型流(IntStream,LongStream,DoubleStream)來避免這種操作,但凡有可能應該使用這些流。
    3. 有些操作本身在并行流上的性能就比順序流差。特別是limit和findFirst等依賴于元素順序的操作。
    4. 還要考慮流的操作流水線的總計算成本。
    5. 對于較小的數據量,選擇并行幾乎從來都不是一個好的決定。并行處理少數幾個元素的好處還抵不上并行化造成的額外開銷。
    6. 要考慮流背后的數據結構是否易于分解。如ArrayList的拆分效率比LinkList高得多,因為前者用不著遍歷就可以平均拆分,而后者則必須遍歷。另外,用range工廠方法創建的原始類型流也可以快速分解。
  1. 分支合并框架

分支合并框架的目的是以遞歸方式將可以并行的任務拆分成更小的任務,然后將每個子任務的結果合并起來生成整體結果。這是ExecutorService接口的一個實現,它把子任務分配給線程池(稱為ForkJoinPool)中的工作線程。

  • 使用RecurisveTask

    要把任務提交到這個池,必須創建RecursiveTask<R>的一個子類,其中R是并行化任務(以及所有子任務)產生的結果類型,或者如果任務不返回結果,則是RecursiveAction類型(當然它可能會更新其他非局部機構)。要定義RecursiveTask,只需要實現它唯一的抽象方法compute;
    protected abstract R compute();
    這個方法同時定義了將任務拆分成子任務的邏輯,以及無法再拆分或不方便再拆分時,生成單個子任務結果的邏輯。偽代碼:

    if(任務足夠小或不可分){
        順序計算該任務
    }else{
        將任務分成兩個子任務
        遞歸調用本方法,拆分每個子任務,等待所有子任務完成
        合并每個子任務的結果
    }
    

選個例子為基礎,讓我們試著用這人框架為一個數字范圍(這里用一個long[]數組表示)求和。你需要先為RecursiveTask類做一個實現:

package com.tim.test;

public class ForkJoinSumCalculator
        extends java.util.concurrent.RecursiveTask<Long> {
    private final long[] numbers;
    private final int start;
    private final int end;
    public static final long THRESHOLD = 10_000;

    public ForkJoinSumCalculator(long[] numbers) {
        this(numbers, 0, numbers.length);
    }

    private ForkJoinSumCalculator(long[] numbers, int start, int end) {
        this.numbers = numbers;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        int length = end - start;
        if (length <= THRESHOLD) {
            return computeSequentially();
        }
        ForkJoinSumCalculator leftTask =
                new ForkJoinSumCalculator(numbers, start, start + length / 2);
        leftTask.fork();
        ForkJoinSumCalculator rightTask =
                new ForkJoinSumCalculator(numbers, start + length / 2, end);
        Long rightResult = rightTask.compute();
        Long leftResult = leftTask.join();
        return leftResult + rightResult;
    }

    private long computeSequentially() {
        long sum = 0;
        for (int i = start; i < end; i++) {
            {
                sum += numbers[i];
            }
            return sum;
        }
    }

測試方法:

    public static long forkJoinSum(long n) {
        long[] numbers = LongStream.rangeClosed(1, n).toArray();
        ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
        return new ForkJoinPool().invoke(task);
    }

運行ForkJoinSumCalculator 當把ForkJoinSumCalculator任務傳給ForkJoinPool時,這個任務就由池中的一個線程執行,這個線程會調用任務的compute方法。該方法會檢查任務是否小到足以順序執行,如果不夠小則會把要求和的數組分成兩半,分給兩個新的ForkJoinSumCalculator,而它們也由ForkJoinPool安排執行。因此,這一過程可以遞歸重復,把原任務分為更小的任務,直到滿足不方便或不可能再進一步拆分的條件。這時會順序計算每個任務的結果,然后由分支過程創建的(隱含的)任務二叉樹遍歷回到它的根。接下來會合并每個子任務的部分結果,從而得到總任務的結果。

  • 使用分支合并框架的最佳做法

    雖然分支/合并框架還算簡單易用,但它容易被誤用。以下是幾個有效使用它的最佳做法:

    1. 對一個任務調用join方法會阻塞調用方,直到該任務做出結果。因此,有必要在兩個子任務的計算開始之后再調用它。否則,你得到的版本會比原始的順序算法更慢更復雜,因為每個子任務都必須等待另一個子任務完成才能啟動。
    2. 不應該在RecursieTask內部使用ForkJoinPool的invoke方法。相反,應該始終直接調用compute或fork方法,只有順序代碼才應該用invoke來啟動并行計算。
    3. 對子任務調用fork方法可以把它排進ForkJoinPool。同時對左邊和右邊的子任務調用它似乎很自然,但這樣做的效率要比直接對其中一個調用compute低。這樣做你可以為其中一個子任務重用同一線程,從而避免在線程池中多分配一個任務造成的開銷。
    4. 調試使用分支/合并框架的并行計算可能有點棘手。特別是你平常都在喜歡的IDE里看棧跟蹤來找問題,但放在分支、合并計算上就不行了,因為調用compute的線程并不是概念上的調用方,后者是調用fork的那個。
    5. 和并行流一樣,你不應理所當然地認為在多核處理器上使用分支合并計算比順序計算快。一個任務可以分解成多個獨立的子任務,才能讓性能在并行化時有所提升。所有這些子任務的運行時間都應該比分出新任務所花的時間長;一個慣用方法是把輸入/輸出放在一個子任務里,計算放在另一個里,這樣計算就可以和輸入/輸出同時進行。此外,在比較同一算法的順序和并行版本的性能時還有別的因素要考慮。就像任何其他Java代碼一樣,分支/合并框架需要“預熱”或者說要執行幾遍才會被JIT編譯器優化。這就是為什么在測量性能之前跑幾遍程序很重要,我們的測試框架就是這么
      做的。同時還要知道,編譯器內置的優化可能會為順序版本帶來一些優勢(例如執行死碼分析——刪去從未被使用的計算)。
  • 工作竊取

    分支/合并框架工程用一種稱為工作竊取的技術解決這個問題。在實際應用中,這意味著這些任務差不多被平均分配到ForkJoinPool中的所有線程上。每個線程都為分配給它的任務保存一個雙向鏈式隊列,每完成一個任務,就會從隊列頭上取出下一個任務開始執行。基于一些原因,某個線程可能早早完成了分配給它的任務,也就是它的隊列已經空了,而其它的線程還很忙。這時,這個線程并沒有閑下來,而是隨機選了一個別的線程從隊列的尾巴上‘偷走’一個任務。這個過程一直繼續下去,直到所有的任務都執行完畢,所有的隊列都清空。這就是為什么要劃成許多小任務而不是少數幾個大任務,這有助于更好地工作線程之間平衡負載。

  1. Spliterator

    Spliterator是Java 8中加入的另一個新接口;這個名字代表“可分迭代器”(splitable
    iterator)。和Iterator一樣,Spliterator也用于遍歷數據源中的元素,但它是為了并行執行而設計的。雖然在實踐中可能用不著自己開發Spliterator,但了解一下它的實現方式會讓你對并行流的工作原理有更深入的了解。Java8已經為集合框架中包含的所有數據結構提供了一個默認的Spliterator實現。集合實現了Spliterator接口,接口提供了一個spliterator方法。這個接口定義了若干方法,如下面的代碼清單所示。

public interface Spliterator<T> {
    boolean tryAdvance(Consumer<? super T> action);
    Spliterator<T> trySplit();
    long estimateSize();
    int characteristics();
}

與往常一樣,T是Spliterator遍歷的元素的類型。tryAdvance方法的行為類似于普通的因為它會按順序一個一個使用Spliterator中的元素,并且如果還有其他元素要遍歷就返回true。但trySplit是專為Spliterator接口設計的,因為它可以把一些元素劃出去分給第二個Spliterator(由該方法返回),讓它們兩個并行處理。Spliterator還可通過estimateSize方法估計還剩下多少元素要遍歷,因為即使不那么確切,能快速算出來是一個值也有助于讓拆分均勻一點。

高效Java8編程

重構,測試和調試

  1. 為改善可讀性和靈活性重構代碼
  • 改善代碼的可讀性
    利用Lambda表達式,方法引用以及Stream改善代碼的可讀性:
    --重構代碼,用Lambda表達式取代匿名類
    --用方法引用重構Lambda表達式
    --用Stream API重構命令式的數據處理。

默認方法

Java8允許在接口內聲明靜態方法。Java8引入了一個新功能,叫默認方法,通過默認方法可以指定接口方法的默認實現。換句話說,接口能提供方法的具體實現。因此,實現接口的類如果不顯式地提供該方法的具體實現,就會自動繼承默認的實現。這種機制可以使你平滑地進行接口的優化和演進。

解決默認方法沖突的三條原則:

  1. 類中的方法優先級最高。類或父類中聲明的方法的優先級高于任何聲明為默認方法的優先級
  2. 如果無法依據第一條進行判斷,那么子接口的優先級更高:函數簽名相同時,優先選擇擁有最具體實現的默認方法的接口,即如果B繼承了A,那么B就比A更具體。
  3. 最后,如果還是無法判斷,繼承了多個接口的類必須通過顯式覆蓋和調用期望的方法,顯式地選擇使用哪一個默認方法的實現 。Java8 引入了一種新的語法X.super.m(...),其中x是你希望調用的m方法的父接口

用Optional取代null

  1. 創建Optional對象
  • 聲明一個空的Optional

可以通過靜態工廠方法Optional.empty,創建一個空的Optional對象:Optional<Car> optCar = Optional.empty()

  • 依據一個非空值創建Optional

使得靜態工廠方法Optional.of,依據一個非空值創建一個Optional對象:
Optional<Car> optcar=Optional.of(car)

  • 可接受null的Optional

使用靜態工廠方法Optional.ofNullable,你可以創建一個允許null值的Optional
對象:
Optional<Car> optCar = Optional.ofNullable(car);
如果car是null,那么得到的Optional對象就是個空對象。

completablerFuture組合式異步編程

  1. future接口

    它建模了一種異步計算,返回一個執行運算結果的引用,當運算結束后,這個引用被返回給調用方。要使用Future,通常你只需要將耗時的操作封裝在一個Callable對象中,再將它提交給ExecutorService,就萬事大吉了

  2. 使用CompletableFuture構建異步應用。

使用supplyAsync創建CompletableFuture對象。

Java 8的 CompletableFuture API提供了名為thenCompose的方法,它就是專門為這一目的而設計的,thenCompose方法允許你對兩個異步操作進行流水線,第一個操作完成時,將其結果作為參數傳遞給第二個操作。換句話說,你可以創建兩個CompletableFutures對象,對第一個CompletableFuture對象調用thenCompose,并向其傳遞一個函數。當第一個CompletableFuture執行完畢后,它的結果將作為該函數的參數,這個函數的返回值是以第一個CompletableFuture的返回做輸入計算出的第二個CompletableFuture對象。

CompletableFuture利用Lambda表達式以聲明式的API提供了一種機制,能夠用最有效的方式,
非常容易地將多個以同步或異步方式執行復雜操作的任務結合到一起

一等函數是可以作為參數傳遞,可以作為結果返回,同時還能存儲在數據結構中的函數。
? 高階函數接受至少一個或者多個函數作為輸入參數,或者返回另一個函數的函數。Java
中典型的高階函數包括comparing、andThen和compose。
? 科里化是一種幫助你模塊化函數和重用代碼的技術。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • lambda表達式(又被成為“閉包”或“匿名方法”)方法引用和構造方法引用擴展的目標類型和類型推導接口中的默認方法...
    183207efd207閱讀 1,499評論 0 5
  • Java 8 lambda表達式示例 轉自importNew 原文鏈接 例1、用lambda表達式實現Runnab...
    低至一折起閱讀 1,380評論 0 3
  • 簡介 概念 Lambda 表達式可以理解為簡潔地表示可傳遞的匿名函數的一種方式:它沒有名稱,但它有參數列表、函數主...
    劉滌生閱讀 3,224評論 5 18
  • 眾所周知,知乎上的三大軟狗,溫趙輪,算是代表了知乎上程序員的最高水平。知乎上的IT牛人還是挺多的,比如,前一段時間...
    向右奔跑閱讀 15,607評論 84 211
  • 人的一生就這么長,可我想把世界裝進身體里。 今天,青小年在這里闡述一個理論:帕金森定律。 官方解釋說:帕金森定律是...
    青小年樹閱讀 1,838評論 0 1