Java8 in action
- 沒有共享的可變數(shù)據(jù)恕齐,將方法和函數(shù)即代碼傳遞給其他方法的能力就是我們平常所說的函數(shù)式編程范式的基石囊卜。
- Collection主要是為了存儲和訪問數(shù)據(jù)析砸,而Stream則主要用于描述對數(shù)據(jù)的計算窿侈。這里的關鍵點在于磷醋,Stream允許并提倡并行處理一個Stream中的元素冕臭。
通過行為參數(shù)化傳遞代碼
- 行為參數(shù)化就是可以幫助你處理頻繁變更的需求的一種軟件開發(fā)模式余蟹。它意味著拿出一個代碼塊卷胯,把它準備好卻不去執(zhí)行它。這個代碼塊以后作為參數(shù)傳遞給另一個方法客叉,稍后再去執(zhí)行它诵竭。
- 行為參數(shù)化,就是一個方法接受多個不同的地為作為參數(shù)兼搏,并在內(nèi)部使用它們卵慰,完成不同行為的能力。行為參數(shù)化可以讓代碼更好地適應不斷變化的要求佛呻,減輕未來的工作量裳朋。
- 傳遞代碼,就是將新行為作為參數(shù)傳遞給方法。但在Java8之前這實現(xiàn)起來很啰嗦吓著。為接口聲明許多只用一次的實體類而造成的啰嗦代碼鲤嫡,在Java8之前可以用匿名類來減少。
Lambda表達式
- 可以把Lambda表達式理解為簡潔地表示可傳遞的匿名函數(shù)的一種方式:
- 匿名-----它不像普通方法那樣有一個明確的名稱绑莺,寫的少而想的多
- 函數(shù)-----我們說它是函數(shù)暖眼,因為Lambda函數(shù)不像方法那樣屬于某個特定的類。但和方法一樣纺裁,Lambda有參數(shù)列表诫肠,函數(shù)主體司澎,返回類型,還可能有可以拋出的異常列表栋豫。
- 傳遞------Lambda表達式可以作為參數(shù)傳遞給方法或存儲在變量中挤安。
- 簡潔------無需像匿名類那樣寫很多模板代碼。
- 在哪里使用Lambda表達式丧鸯,在函數(shù)式接口上使用Lambda表達式
- 函數(shù)式接口:就是只定義一個抽象方法的接口蛤铜。Lambda表達式允許你直接以內(nèi)聯(lián)的形式為函數(shù)式接口的抽象方法提供實現(xiàn),并把整個表達式作為函數(shù)式接口的實例丛肢。你用匿名內(nèi)部類也可以完成同樣的事情围肥,只不過比較笨拙:需要提供一個實現(xiàn),然后再直接內(nèi)聯(lián)將它實例化蜂怎。
- 函數(shù)式接口的抽象方法的簽名基本上就是Lambda表達式的簽名虐先。我們將這種抽象方法叫作函數(shù)描述符。
- 把Lambda付諸實踐:環(huán)繞執(zhí)行模式
- 第一步:記得行為參數(shù)化派敷,傳遞行為正是Lambda的拿手好戲
- 第二步,使用函數(shù)式接口來傳遞行為
- 第三步撰洗,執(zhí)行一個行為 Lambda表達式允許你直接內(nèi)聯(lián)篮愉,為函數(shù)式接口的抽象方法提供實現(xiàn),并且將整個表達式作為函數(shù)式接口的一個實例差导。
- 第四步:傳遞Lambda
- 使用函數(shù)式接口试躏。 Java8的設計師在java.util.function包中引入了幾個新的函數(shù)式接口。
- Predicate java.util.function.Predict<T> 接口定義了一個名為test的抽象方法设褐,它接受泛型T對象颠蕴,并返回一個boolean。在你需要表示一個涉及類型T的布爾表達式時助析,就可以使用這個接口犀被。
- Consumer java.util.function.Consumer<T> 定義了一個名叫accept的抽象方法,它接受泛型T的對象外冀,沒有返回(void).你如果需要訪問類型T對象寡键,并對其執(zhí)行某些操作,就可以使用這個接口雪隧。
- Function java.util.function.Function<T,R>接口定義了一個叫作apply的方法西轩,它接受一個泛型T的對象,并返回一個泛型R的對象脑沿。如果你需要定義一個Lambda藕畔,將輸入對象的信息映射到輸出,就可以使用這個接口庄拇。
- 原始類型特化 上面的三個泛型函數(shù)式接口:Predicate<T>, Consumer<T>和Function<T,R>注服。還有些函數(shù)式接口專為某些類型而設計。 Java類型要么是引用類型(如,Byte,Integer,Object,List)祠汇,要么是原始類型(比如:int,double,byte,char).但是泛型只能綁定到引用類型仍秤。 Java8為我們前面所說的函數(shù)式接口帶來了一個專門的版本,以便在輸入和輸出都是原始類型時避免自動裝箱的操作可很。 一般來說诗力,針對專門的輸入?yún)?shù)類型的函數(shù)式接口的名稱都要加上對應的原始類型前綴,如DoublePredicate,IntConsumer,LongBinaryOperator,IntFunction等我抠。Function接口還有針對輸出參數(shù)類型的變種:ToIntFunction<T>,IntToDoubleFunction等
- 類型檢查苇本,類型推斷以及限制
- 類型檢查 Lambda的類型是從使用Lambda的上下文推斷出來的。上下文(比如菜拓,接受它傳遞的方法的參數(shù)瓣窄,或接受它的值的局部變量)中Lambda表達式需要的類型稱為目標類型。
- 同樣的Lambda纳鼎,不同的函數(shù)式接口俺夕。有了目標類型的概念,同一個Lambda表達式就可以與不同的函數(shù)式接口聯(lián)系起來贱鄙,只要它們的抽象方法簽名能夠兼容劝贸。 -------特殊的void兼容規(guī)則 如果一個Lambda的主體是一個語句表達式,它就和一個返回void的函數(shù)描述符兼容(當然需要參數(shù)列表也兼容)逗宁。如映九,以下兩行都是合法的,盡管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編譯器會從上下文(目標類型)推斷出用什么函數(shù)式接口來配合Lambda表達式瞎颗,這意味著它也可以推斷出適合Lambda的簽名件甥,因為函數(shù)描述符可以通過目標類型來得到。這樣做的好處在于哼拔,編譯器可以了解Lambda表達式的參數(shù)類型引有,這樣就可以在Lambda語法中省去標注參數(shù)類型。
- 使用局部變量管挟。 Lambda表達式也允許使用自由變量(不是參數(shù)轿曙,而是在外層作用域中定義的變量),就像匿名類一樣僻孝。它們被稱作捕獲Lambda. 關于能做這些變量做什么有一些限制导帝。Lambda可以沒有限制地捕獲(也就是在其主體中引用)實例變量和靜態(tài)變量。但局部變量必須聲明為final,或事實上是final.換句話說穿铆,Lambda表達式只能捕獲指派給它們的局部變量一次您单。(注:捕獲實例變量可以被看作捕獲最終局部變量this)
- 方法引用 方法引用讓你可以重復使用現(xiàn)有的方法定義,并像Lambda一樣傳遞它們荞雏。方法引用就是讓你根據(jù)已有的方法實現(xiàn)來創(chuàng)建Lambda表達式虐秦∑侥穑可以把方法引用 看作針對僅僅涉及單一方法的Lambda語法糖,因為你表達同樣的事情時要寫的代碼更少了悦陋。方法引用主要有三類
- 指向靜態(tài)方法的方法引用
- 指向任意類型實例方法的方法引用(如String的length方法)
- 指向現(xiàn)有對象的實例方法的引用
- 構造函數(shù)引用 對于一個現(xiàn)有的構造函數(shù)蜈彼,你可以利用它的名稱和關鍵字new來創(chuàng)建它的一個引用:ClassName:new。 它的功能與指向靜態(tài)方法的引用類似俺驶。
- 在需要函數(shù)式接口的地方可以使用Lambda表達式幸逆。函數(shù)式接口就是僅僅定義一個抽象方法的接口。抽象方法的簽名(稱為函數(shù)描述符)描述了Lambda表達式的簽名暮现。
- 復合Lamda表達式的有用方法还绘。 Java8的好幾個函數(shù)式接口都有為方便而設計的方法。具體而言栖袋,許多函數(shù)式接口拍顷,比如用于傳遞Lambda表達式的Comparator,Function和Predicate都提供了允許你進行復合的方法。 在實踐中塘幅,這意味著你可以把簡單的Lambda復合成復雜的表達式昔案。如,你可以讓兩個謂詞之間做一個or操作电媳,組合成一個更大的謂詞爱沟。而且,你可以讓一個函數(shù)的結果成為另一個函數(shù)的輸入匆背。你可能會想,函數(shù)式接口中怎么可能有更多的方法呢身冀?竅門在于钝尸,我們即將介紹的方法都是默認方法,也就是說它們不是抽象方法搂根。
- 謂詞復合 謂詞接口包括三個方法:negate,and和or,讓重用已有的Predicate來創(chuàng)建更復雜的謂詞珍促。如,你可使用negate方法返回一個Predicate的非剩愧。
- 函數(shù)復合 你還可以把Function接口所代表的Lambda表達式復合起來猪叙。Funcation接口為此配了andThen和compose兩個默認方法,它們都返回Function的一個實例仁卷。andThen方法會返回一個函數(shù)穴翩,它先對輸入應用一個給定函數(shù),再對輸出應用另一個函數(shù)锦积。你也可以類似地使用compose方法芒帕,先把給定的函數(shù)用作compose的參數(shù)里面給的那個函數(shù),然后再把函數(shù)本身用于結果丰介。
- 判斷一個操作是惰性求值還是及早求值很簡單:只需看它的返回值背蟆。如果返回值是Stream鉴分,那么是惰性求值,如果返回值是另一個值或為空带膀,那么就是及早求值志珍。使用這些操作的理想方式就是形成一個惰性求值的鏈,最后用一個及早求值的操作返回想要的結果垛叨。
小結:
- Lambda表達式可以理解為一種匿名函數(shù)伦糯,它沒有名稱,但有參數(shù)列表点额,函數(shù)主體舔株,返回類型,可能還有一個可以拋出的異常列表
- 只有在接受函數(shù)式接口的地方才可以使用Lambda表達式
- Lambda表達式允許你直接內(nèi)聯(lián)还棱,為函數(shù)式接口的抽象方法提供實現(xiàn)载慈,并且將整個表達式作為函數(shù)式接口的一個實例。
- 為了避免裝箱操作珍手,對Predicate<T>和Function<T,R>等通用函數(shù)式接口的原始類型特化:IntPredicate,IntToLongFunction等办铡。
- 環(huán)繞執(zhí)行模式(即在方法所必須的代碼中間,你需要執(zhí)行點什么操作琳要,如資源分配 和清理)可以配合Lambda提高靈活性和可重用性寡具。
- Lambda表達式所需要代表的類型稱為目標類型
函數(shù)式數(shù)據(jù)處理
引入流
- 流是什么 流是Java API的新成員,它允許你以聲明方式處理數(shù)據(jù)集合
- Java8中的Stream API可以讓你寫出這樣的代碼:聲明性--更簡潔稚补,更易讀; 可復合--更靈活童叠; 可并行--性能更好
- 流簡介 流到底是什么呢?簡短的定義就是“從支持數(shù)據(jù)處理操作的源生成的元素序列”。讓我們一步步剖析這個定義课幕。
- 元素序列 像集合一樣厦坛,流也提供了一個接口,可以訪問特定元素類型的一組有序值乍惊。因為集合是數(shù)據(jù)結構杜秸,主要目的是以特定的時間/空間復雜度存儲和訪問元素(如ArrayList).但流的目的在于表達計算,如filter,sorted和map.集合講的是數(shù)據(jù)润绎,流講的是計算
- 源: 流會使用一個提供數(shù)據(jù)的源撬碟,如集合,數(shù)組或輸入/輸出資源
- 數(shù)據(jù)處理操作 流的數(shù)據(jù)處理功能支持類似于數(shù)據(jù)庫的操作莉撇,以及函數(shù)式編程語言中的常用操作呢蛤,如filter,map,reduce,find,match,sort等。流操作可以順序執(zhí)行,也可并行執(zhí)行。
- 流與集合
- 粗糙地說汪厨,集合與流之間的差異就在于什么時候進行計算。集合是一個內(nèi)存中的數(shù)據(jù)結構静秆,它包含數(shù)據(jù)結構中目前所有的值---集合中的每個元素都得先算出來才能添加到集合中粮揉。相比之下,流則是在概念上固定的數(shù)據(jù)結構(你不能添加或刪除元素)抚笔,其元素則是按需計算的扶认。
- 只能遍歷一次 和迭代器一樣,流只能遍歷一次殊橙。遍歷完之后辐宾,我們就說這個流已經(jīng)被消費掉了。
- 外部迭代與內(nèi)部迭代 Streams庫使用內(nèi)部迭代---它幫你把迭代做了膨蛮,還把得到的流值存在了某個地方
- 流操作 java.util.stream.Stream中的Stream接口定義了許多操作叠纹。它們可以分為兩大類。
- 中間操作 如filter或sorted等中間操作會返回另一個流敞葛。這讓多個操作可以連接起來形成一個查詢誉察。
- 終端操作 終端操作會從流的流水線生成結果。其結果是任何不是流的值惹谐,如List,Integer,甚至void
- 使用流 一般三件事:1--一個數(shù)據(jù)源來執(zhí)行一個查詢持偏。 2--一個中間操作鏈,形成一條流的流水線 3--一個終端操作氨肌,執(zhí)行流水線鸿秆,并能生成結果。
使用流
- 篩選和切片
- 用謂詞篩選 Stream接口支持filter方法怎囚。該操作會接受一個謂詞(一個返回boolean的函數(shù))作為參數(shù)卿叽,并返回一個包括所有符合謂詞的元素的流。
- 篩選各異的元素 流還支持一個叫作distinct的方法恳守,它會返回一個元素各異(根據(jù)元素的上hashcode和equels方法)的流附帽。
- 截斷流 流支持limit(n)方法,會返回一個不超過給定長度的流井誉。
- 跳過元素 流還支持skip(n)方法,返回一個扔掉了前n個元素的流整胃。如果流中元素不足n個颗圣,則返回一個空流。
- 映射 一個非常常見的數(shù)據(jù)處理套路就是從某些對象中選擇信息屁使。比如在SQL里在岂,你可以從表里選擇一列。Stream API也通過map和flatMap方法提供了類似的工具蛮寂。
- 對流中每一個元素應用函數(shù) 流支持map方法蔽午,它會接受一個函數(shù)作為參數(shù)。這個函數(shù)會被應用到每個元素上酬蹋,并將映射成一個新的元素
- 流的扁平化 flatmap方法讓你把一個流中的每個值都換成另一個流及老,然后把所有的流連接起來成為一個流抽莱。
- 查找和匹配 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) 會在值存在的時候執(zhí)行給定的代碼塊寞秃。
--T get() 會在值存在時返回值斟叼,否則拋出一個NoSuchElement異常
--T orElse(T other)會在值存在時返回值,否則返回一個默認值春寿。 查找第一個元素 findFirst
何時使用findFirst和findAny ,為什么會同時有findFirst和findAny?答案是并行朗涩。找到第一個元素在并行上限制更多。如果你不關心返回的元素是哪個堂淡,請使用findAny()
- 歸約 如何把一個流中的元素組合起來馋缅,使用reduce操作來表達更復雜的查詢,如“計算菜單中的總卡路里”或“菜單中卡路里最高的菜是哪一個”绢淀。此類查詢需要將流中所有元素反復結合起來萤悴,得到一個值,比如一個Integer.這樣的查詢可以被歸類為歸約操作(將流歸約成一個值)皆的。用函數(shù)式編程語言的術語來說覆履,這稱為折疊(fold)
- 數(shù)值流
- 原始類型流特化。 Java8引入了三個原始類型特化流接口來解決一般裝箱拆箱問題:intStream,DoubleStream和LongStream,分別將流中的元素特化為int,long和double,從而避免了暗含的裝箱成本费薄。每個接口都帶來了進行常用數(shù)值歸約的新方法硝全,如sum,max
---1. 映射到數(shù)值流,將流轉(zhuǎn)化為特化版本的常用方法是mapToInt,mapToDouble和mapToLong楞抡。這些方法和前面說的map方法的工作方式一樣伟众,只是它們返回的是一個特化流,而不是Stream<T>
---2. 轉(zhuǎn)換回對象流 有了數(shù)值流召廷,你可能會想把它轉(zhuǎn)換回非特化流凳厢。可以用boxed方法
---3. 默認值OptionlInt竞慢。 Optional可以用Integer,String等參考類型來參數(shù)化先紫。對于三種原始流特化,也分別有一個Optional原始類型特化版本:OptionalInt,OptionalDouble和OptionalLong.
- 數(shù)值范圍 和數(shù)字打交道時筹煮,有一個常用的東西就是數(shù)值范圍遮精,如,假設你想要生成1和100之間的所有數(shù)字败潦。Java8引入了兩個可以用于IntStream和LongStream的靜態(tài)方法本冲,幫助生成這種范圍:rang和的rangeClosed.這兩個方法都是第一參數(shù)接受起始值准脂,第二個參數(shù)接受結束值。但range是不包含結束值的眼俊,而rangeClosed則包含結束值意狠。
- 構建流
-
由值創(chuàng)建流
可以使用靜態(tài)方法Stream.of 通過顯式值創(chuàng)建一個流。它可以接受任意數(shù)量的參數(shù)疮胖。
-
由數(shù)組創(chuàng)建流
你可以使用靜態(tài)方法Arrays.stream從數(shù)組創(chuàng)建一個流环戈。它接受一個數(shù)組作為參數(shù)。
-
由文件生成流
java中用于處理文件等I/O操作的NIO API已更新澎灸,以便利用Stream API. java.nio.file.Files中很多靜態(tài)方法都會返回一個流院塞。 如Files.lines,它會返回一上由指定文件中的各行構成的字符串流。
-
由函數(shù)生成的流:創(chuàng)建無限流
Stream API提供了兩個靜態(tài)方法來從函數(shù)生成流:Stream.iterate和Stream.generate. 這兩個操作可以創(chuàng)建所謂的無限流:不像從固定集合創(chuàng)建的流那樣有固定大小的流性昭。由iterate和generate產(chǎn)生的流會用給定的函數(shù)按需創(chuàng)建值拦止,因此可以無窮地計算下去!一般來說糜颠,應該使用limit(n)來對這種流加以限制汹族。
---迭代 如:Stream.iterate(0,n->n+2)。 生成偶數(shù)流其兴。一般來說顶瞒,在需要依次生成一系列值的時候應該使用iterate.
---生成 與iterate方法類似,generate方法也可讓你按需生成一個無限流元旬,但generate不是依次每個新生成的值應用函數(shù)的榴徐。它接受一個Supplier<T>類型的Lambda提供新的值。如 Stream.generate(Math::random)
用流收集數(shù)據(jù)
- 收集器簡介
- 收集器用作高級歸約
收集器非常有用匀归,用它可以簡潔而靈活地定義collect用來生成的集合的標準坑资。一般來說,Collector會元素應用一個轉(zhuǎn)換函數(shù)(很多時候是不體現(xiàn)任何效果的恒等轉(zhuǎn)換穆端,如toList),并將結果累積到一個數(shù)據(jù)結構中袱贮,從而產(chǎn)生這一過程的最終輸出。 - 預定義收集器
預定義收集器也就是那些從Collectors類提供的工廠方法(如groupBy)創(chuàng)建的收集器体啰。它們主要提供三大功能:--將流元素歸約和匯總為一個值 --元素分組 --元素分區(qū)
- 歸約和匯總
查找流中最大值和最小值 Collectors.maxBy和Colectors.minBy,來計算流中的最大和最小值攒巍。這兩個收集器接收一個Comparator參數(shù)來比較流中的元素。
匯總 Collecgors類專門為匯總提供了一個工廠方法:Collectors.summingInt狡赐。它可接受一個把對象映射為求和所需要int的函數(shù),并返回一個收集器钦幔。該收集器傳遞給普通的collect方法后即可執(zhí)行我們需要的匯總操作枕屉。 Collectors.summingLong和Collectors.summingDouble方法的作用完全一樣,可用于求和字段為long和double的情況鲤氢。 但匯總不僅僅是求和:還有Collectors.averagingInt,連同對應的averagingLong和averaingDouble可以計算數(shù)值的平均數(shù)搀擂。 目前為止西潘,你已經(jīng)看到了如何使用收集器來給流中的元素計數(shù),找到這些元素數(shù)值屬性的最大值和最小值哨颂,以及計算其總和和平均值喷市。不過很多時候,你可能想要得到兩個或更多這樣的的結果威恼,而且你希望只需要一次操作就可以完成品姓。在這種情況下,可以使用summarizingInt工廠方法返回的收集器箫措。如:IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingIng(Dish::getCalories));
這個收集器會把所有這些信息收集到一個叫作IntSummaryStatistics類里腹备,它提供了方便的取值(getter)方法來訪問結果。連接字符串 joining工廠方法返回的收集器會把流中每一個對象應用toString方法得到所有字符串連接成一個字符串斤蔓。 joining內(nèi)部使用了StringBuilder來把生成的字符串逐個追加起來植酥。但該字符串的可讀性并不好,joining工廠方法有一個重載版本可以接受元素分界符joining(",").
到目前為止弦牡,我們已經(jīng)探討了各種將流歸約互一個值的收集器友驮。下一節(jié),我們會展示為什么所有這種形式的歸約過程驾锰,其實都是Collectors.reducing工廠方法提供的更廣義歸約收集器的特殊情況卸留。廣義的歸約匯總
事實上,我們已經(jīng)討論的所有收集器稻据,都是一個可以用reducing工廠方法定義的歸納過程的特殊情況而已艾猜。Collectors.reducing工廠方法是所有這些特殊情況的一般化。 它需要三個參數(shù)
第一個參數(shù)是歸約操作的起始值捻悯,也中流中沒有元素時的返回值匆赃,所以很顯然對于數(shù)值和而言0是一個合適的值
第二個參數(shù)就是一個轉(zhuǎn)化函數(shù),如將菜肴轉(zhuǎn)化成一個表示其所含熱量的int
第三個參數(shù)是一個BinaryOperator,將兩個項目累加成一個同類型的值今缚。如兩個int的和
同樣算柳,你可以使用下面的單參數(shù)形式的reducing來找到熱量最高的菜。如下所示:
Optional<Dish> mostCalorieDish =menu.stream().collect(reducing((d1,d2)->d1.getCalories()>d2.getCalories()?d1:d2));
可以把單參數(shù)reducing工廠方法創(chuàng)建的收集器看作三參數(shù)方法的特殊情況姓言,它把流中的第一個項目作為起點瞬项,把恒等函數(shù)(即一個函數(shù)僅僅是返回其輸入?yún)?shù))作為一個轉(zhuǎn)換函數(shù)。這也意味著何荚,要是把單參數(shù)reducing收集器傳遞給空流的collect方法囱淋,收集器就沒有起點。它將因此返回一個Optional<Dish>對象
從邏輯上說餐塘,歸約操作的工作原理:利用累積函數(shù)妥衣,把一個初始化為起始值的累加器,和轉(zhuǎn)換函數(shù)應用到流中每個元素上得到的結果不斷迭代合并起來。
- 分組
一個常見的數(shù)據(jù)為操作是根據(jù)一個或多個屬性對集合中的項目進行分組税手。用Collectors.groupingBy工廠方法返回的收集器就可以輕松地完成這項任務蜂筹。groupBy接收一個分類函數(shù),用它來把流中的元素分成不同的組芦倒。把分組函數(shù)返回的值作為映射的鍵艺挪,把流中所有具有這個分類值的項目的列表作為對應的映射值。值就是包含所有對應類型的列表兵扬。
- 多級分組
要實現(xiàn)多級分組麻裳,我們可以使用一個由雙參數(shù)版本的Collectors.groupingBy工廠方法創(chuàng)建的收集器,它除了普通的分類函數(shù)之外周霉,還可以接受collector類型的第二個參數(shù)掂器,那么要時行二級分組的話,我們可以把內(nèi)層groupBy傳遞給外層groupingBy,并定義一個為流中項目分類的二級標準俱箱。二級分組的結果是兩級map.
- 按子組收集數(shù)據(jù)
上面的小節(jié)国瓮,可以把第二個groupingBy收集器傳遞給外層收集器來實現(xiàn)多級分組。但進一步說狞谱,傳遞給第一個groupingBy的第二個收集器可以是任何類型乃摹,而不一定是另一個groupingBy. 如,要數(shù)一數(shù)菜單中每類菜有多少個跟衅,可以傳遞counting收集作為groupingBy收集器的第二個參數(shù)孵睬。
Map<Dish.Type,Long> typesCount = menu.stream().collect(groupingBy(Dish::getType,counting()));
注意普通的單參數(shù)groupingBy(f)(其中f是分類函數(shù))實際上是groupingBy(f,toList())的簡便寫法。
---1. 把收集器的結果轉(zhuǎn)換為另一種類型伶跷。 因為分組操作的Map結果中的每個值上包裝的Optional沒什么用掰读,所 你可能想把它們?nèi)サ簟R龅竭@一點叭莫,或者更一般地來說蹈集,把收集器返回的結果轉(zhuǎn)換為另一種類型,你可以使用Collectors.collectingAndThen工廠方法返回的收集器雇初。
Map<Dish.Type, Dish> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),
Optional::get)));
這個工廠方法接受兩個參數(shù)---要轉(zhuǎn)換的收集器以及轉(zhuǎn)換函數(shù)拢肆,并返回另一個收集器。這個收集器相當于舊收集器的一個包裝靖诗,collect操作的最后一步就是將返回值用轉(zhuǎn)換函數(shù)做一個映射郭怪。在這里,被包起來的收集器就是用maxBy建立的那個刊橘,而轉(zhuǎn)換函數(shù)Optional::get則把返回的Optional中的值提取出來鄙才。這個操作放在這里是安全的,因為reducing收集器永遠不會返回Optional.empty().
把收集器嵌套起來很常見促绵,它們從外層逐層向里有以下幾點:
1. groupingBy是最外層攒庵,根據(jù)菜肴的類型把菜單流分組据途,得到 三個子流
2. groupingBy收集器包裹著collectingAndThen收集器,因此分組操作得到的每個子流都用這第二個收集器做進一步歸約叙甸。
3. collectingAndThen收集器又包裹著第三個收集器maxBy
4. 隨后由歸約收集器進行子流的歸約操作,然后包含它的collectingAndThen收集器會對其結果應用Optional:get轉(zhuǎn)換函數(shù)
5. 對三個子流分別執(zhí)行這一過程并轉(zhuǎn)換而得到的三個值位衩,也就是各個類型中熱量最高的Dish,將成為groupingBy收集器返回的Map中與各個分類鍵(Dish的類型)相關聯(lián)的值裆蒸。
---2. 與groupingBy聯(lián)合使用的其他收集器的例子
通過groupingBy工廠方法的第二個參數(shù)傳遞的收集器將會對分到同一組中的所有流元素執(zhí)行進一步歸約操作。如:你還重用求出所有菜肴熱量總和的收集器糖驴,不過這次是對每一組Dish求和:
Map<Dish.Type,Integer> totalCaloriesByType = menu.stream().collect(groupingBy(Dish::getType,summingInt(Dish::getCalories)));
然而常常和groupingBy聯(lián)合使用的另一個收集器是mapping方法生成的僚祷。這個方法接受兩個參數(shù):一個函數(shù)對流中的元素做變換,另一個則將變換的結果對象收集起來贮缕。其目的是在累加之前對每個輸入元素應用一個映射函數(shù)辙谜,這樣就要可以讓接受特定類型元素的收集器適應不同類型的對象。 例子:比方說你想要知道感昼,對于每種類型的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())
)
);
- 分區(qū)
分區(qū)是分組的特殊情況:由一個謂詞(返回一個布爾值的函數(shù))作為分類函數(shù),它稱分區(qū)函數(shù)定嗓。分區(qū)函數(shù)返回一個布爾值蜕琴,這意味著得到的分組Map的鍵類型是Boolean
- 分區(qū)的優(yōu)勢
分區(qū)的好處在于保留了分區(qū)函數(shù)返回true或false的兩套流元素列表。partitioningBy工廠方法還有一個重載版本宵溅,可以傳遞第第二個收集器
- 收集器接口
Collector接口包含了一系列方法凌简,為實現(xiàn)具體的歸約操作(即收集器)提供了范本。
public interface Collector<T,A,R>{
Supplier<A> suppler();
BiConsumer<A,T> accumulator();
Function<A,R> finisher();
Set<Characteristics> characteristics();
}
本列表適用以下定義恃逻。
T是流中要收集的項目的泛型雏搂。
A是累加器的類型,累加器是在收集過程中用于累加部分結果的對象寇损。
R是收集操作得到的對象(通常但并不一定是集合)的類型凸郑。
- 理解Collector接口聲明的方法
-
建立新的結果容器:supplier方法
supplier方法必須返回一個結果為空的Supplier,也就是一個無參數(shù)函數(shù)润绵,在調(diào)用時它會創(chuàng)建一個空的累加器實例线椰,供數(shù)據(jù)收集過程使用。
-
將元素添加到結果容器:accumulator方法
accumulator方法會返回執(zhí)行歸約操作的函數(shù)尘盼。當遍歷到流中第n個元素時憨愉,這個函數(shù)執(zhí)行時會有兩個參數(shù):保存歸約結果的累加器(已收集了流中前n-1個項目),還有第n個元素本身卿捎。該函數(shù)將返回void,因為累加器是原位更新配紫,即函數(shù)的執(zhí)行改變了它的內(nèi)部狀態(tài)以體現(xiàn)遍歷的元素的效果。
-
對結果容器應用最終轉(zhuǎn)換:finisher方法
在遍歷完流后午阵,finisher方法必須返回在累加過程的最后要調(diào)用的一個函數(shù)躺孝,以便將累加器對象轉(zhuǎn)換為整個集合操作的最終結果享扔。
這三個方法已經(jīng)足以對流進行順序歸約。實踐中的實現(xiàn)細節(jié)可能還要復雜一點植袍,一方面是因為流的延遲性質(zhì)惧眠,可能在collect操作之前還需要完成其他中間操作的流水線,另一方面則是理論上可能要進行并行歸約于个。
-
合并兩個結果容器:combiner方法
四個方法中的最后一個-----combiner方法會返回一個供歸約操作使用的函數(shù)氛魁,它定義了對流的各個子部分進行并行處理時,各個子部分歸約所得的累加器要如何合并厅篓。
有了這個第四個方法秀存,就可以對流進行并行歸約了。它會用到Java7中引入的分支/合并框架和Spliterator抽象羽氮。
-
characteristics方法
characteristics會返回一個不可變的Characteristics集合或链,它定義了收集器的行為----特別是關于流是否可以并行歸約,以及可以使用哪些優(yōu)化的提示档押。Characteristics是一個包含三個項目的枚舉:
---UNORDRED--歸約結果不受流中項目的遍歷和累積順序的影響
---CONCURRENT--accumulator函數(shù)可以從多個線程同時調(diào)用澳盐,且該收集器可以并行歸約流。如果收集器沒有標為UNORDERED,那它僅在用于無序數(shù)據(jù)源時才可以并行歸約令宿。
---IDENTITY_FINISH--這表明完成器方法返回的函數(shù)是一個恒等函數(shù)洞就,可以跳過。這種情況下掀淘,累加器對象將會直接用作歸約過程的最終結果旬蟋。這也意味著,將累加器A不加檢查地轉(zhuǎn)換為結果R是安全的革娄。
并行數(shù)據(jù)處理與性能
-
并行流
Stream接口可以通過收集源調(diào)用parallelStream方法來把集合轉(zhuǎn)換為并行流倾贰。并行流就是把一個內(nèi)容分成多個數(shù)據(jù)塊,并用不同的線程分別處理每個數(shù)據(jù)塊的流拦惋。
- 并行流內(nèi)部使用了默認的ForkJoinPool,它默認的線程數(shù)量就是你的處理器數(shù)量匆浙,這個值是由Runtime.getRunTime().availableProcessors()得到的。但是你可以通過系統(tǒng)屬性 java.util.concurrent.ForkJoinPool.common.parallelism來改變線程池大小厕妖。如下:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
使用正確的數(shù)據(jù)結構然后使其并行化工作能保證最佳的性能首尼。特別注意原始類型的裝箱和拆箱操作。
-
高效使用并行流
一些幫你決定某個特定情況下是否有必要使用并行流的建議:
- 有疑問言秸,測量软能。把順序流轉(zhuǎn)化成并行流輕而易舉,但卻不一定是好事举畸。并行流并不總是比順序流快查排。
- 留意裝箱。自動裝箱和拆箱操作會大大降低性能抄沮。Java8中有原始類型流(IntStream,LongStream,DoubleStream)來避免這種操作跋核,但凡有可能應該使用這些流岖瑰。
- 有些操作本身在并行流上的性能就比順序流差。特別是limit和findFirst等依賴于元素順序的操作砂代。
- 還要考慮流的操作流水線的總計算成本蹋订。
- 對于較小的數(shù)據(jù)量,選擇并行幾乎從來都不是一個好的決定刻伊。并行處理少數(shù)幾個元素的好處還抵不上并行化造成的額外開銷辅辩。
- 要考慮流背后的數(shù)據(jù)結構是否易于分解。如ArrayList的拆分效率比LinkList高得多娃圆,因為前者用不著遍歷就可以平均拆分,而后者則必須遍歷蛾茉。另外讼呢,用range工廠方法創(chuàng)建的原始類型流也可以快速分解。
- 分支合并框架
分支合并框架的目的是以遞歸方式將可以并行的任務拆分成更小的任務谦炬,然后將每個子任務的結果合并起來生成整體結果悦屏。這是ExecutorService接口的一個實現(xiàn),它把子任務分配給線程池(稱為ForkJoinPool)中的工作線程键思。
-
使用RecurisveTask
要把任務提交到這個池础爬,必須創(chuàng)建RecursiveTask<R>的一個子類,其中R是并行化任務(以及所有子任務)產(chǎn)生的結果類型吼鳞,或者如果任務不返回結果看蚜,則是RecursiveAction類型(當然它可能會更新其他非局部機構)。要定義RecursiveTask,只需要實現(xiàn)它唯一的抽象方法compute;
protected abstract R compute();
這個方法同時定義了將任務拆分成子任務的邏輯赔桌,以及無法再拆分或不方便再拆分時供炎,生成單個子任務結果的邏輯。偽代碼:if(任務足夠小或不可分){ 順序計算該任務 }else{ 將任務分成兩個子任務 遞歸調(diào)用本方法疾党,拆分每個子任務音诫,等待所有子任務完成 合并每個子任務的結果 }
選個例子為基礎,讓我們試著用這人框架為一個數(shù)字范圍(這里用一個long[]數(shù)組表示)求和雪位。你需要先為RecursiveTask類做一個實現(xiàn):
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時竭钝,這個任務就由池中的一個線程執(zhí)行,這個線程會調(diào)用任務的compute方法雹洗。該方法會檢查任務是否小到足以順序執(zhí)行香罐,如果不夠小則會把要求和的數(shù)組分成兩半,分給兩個新的ForkJoinSumCalculator,而它們也由ForkJoinPool安排執(zhí)行时肿。因此穴吹,這一過程可以遞歸重復,把原任務分為更小的任務嗜侮,直到滿足不方便或不可能再進一步拆分的條件港令。這時會順序計算每個任務的結果啥容,然后由分支過程創(chuàng)建的(隱含的)任務二叉樹遍歷回到它的根。接下來會合并每個子任務的部分結果顷霹,從而得到總任務的結果咪惠。
-
使用分支合并框架的最佳做法
雖然分支/合并框架還算簡單易用,但它容易被誤用淋淀。以下是幾個有效使用它的最佳做法:
- 對一個任務調(diào)用join方法會阻塞調(diào)用方遥昧,直到該任務做出結果。因此朵纷,有必要在兩個子任務的計算開始之后再調(diào)用它炭臭。否則,你得到的版本會比原始的順序算法更慢更復雜袍辞,因為每個子任務都必須等待另一個子任務完成才能啟動鞋仍。
- 不應該在RecursieTask內(nèi)部使用ForkJoinPool的invoke方法。相反搅吁,應該始終直接調(diào)用compute或fork方法威创,只有順序代碼才應該用invoke來啟動并行計算。
- 對子任務調(diào)用fork方法可以把它排進ForkJoinPool谎懦。同時對左邊和右邊的子任務調(diào)用它似乎很自然肚豺,但這樣做的效率要比直接對其中一個調(diào)用compute低。這樣做你可以為其中一個子任務重用同一線程界拦,從而避免在線程池中多分配一個任務造成的開銷吸申。
- 調(diào)試使用分支/合并框架的并行計算可能有點棘手。特別是你平常都在喜歡的IDE里看棧跟蹤來找問題享甸,但放在分支呛谜、合并計算上就不行了,因為調(diào)用compute的線程并不是概念上的調(diào)用方枪萄,后者是調(diào)用fork的那個隐岛。
- 和并行流一樣,你不應理所當然地認為在多核處理器上使用分支合并計算比順序計算快瓷翻。一個任務可以分解成多個獨立的子任務聚凹,才能讓性能在并行化時有所提升。所有這些子任務的運行時間都應該比分出新任務所花的時間長齐帚;一個慣用方法是把輸入/輸出放在一個子任務里妒牙,計算放在另一個里,這樣計算就可以和輸入/輸出同時進行对妄。此外湘今,在比較同一算法的順序和并行版本的性能時還有別的因素要考慮。就像任何其他Java代碼一樣剪菱,分支/合并框架需要“預熱”或者說要執(zhí)行幾遍才會被JIT編譯器優(yōu)化摩瞎。這就是為什么在測量性能之前跑幾遍程序很重要拴签,我們的測試框架就是這么
做的。同時還要知道旗们,編譯器內(nèi)置的優(yōu)化可能會為順序版本帶來一些優(yōu)勢(例如執(zhí)行死碼分析——刪去從未被使用的計算)蚓哩。
-
工作竊取
分支/合并框架工程用一種稱為工作竊取的技術解決這個問題。在實際應用中上渴,這意味著這些任務差不多被平均分配到ForkJoinPool中的所有線程上岸梨。每個線程都為分配給它的任務保存一個雙向鏈式隊列,每完成一個任務稠氮,就會從隊列頭上取出下一個任務開始執(zhí)行曹阔。基于一些原因隔披,某個線程可能早早完成了分配給它的任務赃份,也就是它的隊列已經(jīng)空了,而其它的線程還很忙锹锰。這時,這個線程并沒有閑下來漓库,而是隨機選了一個別的線程從隊列的尾巴上‘偷走’一個任務恃慧。這個過程一直繼續(xù)下去,直到所有的任務都執(zhí)行完畢渺蒿,所有的隊列都清空痢士。這就是為什么要劃成許多小任務而不是少數(shù)幾個大任務,這有助于更好地工作線程之間平衡負載茂装。
-
Spliterator
Spliterator是Java 8中加入的另一個新接口怠蹂;這個名字代表“可分迭代器”(splitable
iterator)。和Iterator一樣少态,Spliterator也用于遍歷數(shù)據(jù)源中的元素城侧,但它是為了并行執(zhí)行而設計的。雖然在實踐中可能用不著自己開發(fā)Spliterator彼妻,但了解一下它的實現(xiàn)方式會讓你對并行流的工作原理有更深入的了解嫌佑。Java8已經(jīng)為集合框架中包含的所有數(shù)據(jù)結構提供了一個默認的Spliterator實現(xiàn)。集合實現(xiàn)了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編程
重構肯骇,測試和調(diào)試
- 為改善可讀性和靈活性重構代碼
- 改善代碼的可讀性
利用Lambda表達式,方法引用以及Stream改善代碼的可讀性:
--重構代碼祖很,用Lambda表達式取代匿名類
--用方法引用重構Lambda表達式
--用Stream API重構命令式的數(shù)據(jù)處理笛丙。
默認方法
Java8允許在接口內(nèi)聲明靜態(tài)方法。Java8引入了一個新功能假颇,叫默認方法鸵赫,通過默認方法可以指定接口方法的默認實現(xiàn)豪治。換句話說,接口能提供方法的具體實現(xiàn)。因此窘俺,實現(xiàn)接口的類如果不顯式地提供該方法的具體實現(xiàn),就會自動繼承默認的實現(xiàn)拓萌。這種機制可以使你平滑地進行接口的優(yōu)化和演進碍沐。
解決默認方法沖突的三條原則:
- 類中的方法優(yōu)先級最高。類或父類中聲明的方法的優(yōu)先級高于任何聲明為默認方法的優(yōu)先級
- 如果無法依據(jù)第一條進行判斷激涤,那么子接口的優(yōu)先級更高:函數(shù)簽名相同時拟糕,優(yōu)先選擇擁有最具體實現(xiàn)的默認方法的接口,即如果B繼承了A,那么B就比A更具體倦踢。
- 最后送滞,如果還是無法判斷,繼承了多個接口的類必須通過顯式覆蓋和調(diào)用期望的方法辱挥,顯式地選擇使用哪一個默認方法的實現(xiàn) 犁嗅。Java8 引入了一種新的語法X.super.m(...),其中x是你希望調(diào)用的m方法的父接口
用Optional取代null
- 創(chuàng)建Optional對象
- 聲明一個空的Optional
可以通過靜態(tài)工廠方法Optional.empty,創(chuàng)建一個空的Optional對象:Optional<Car> optCar = Optional.empty()
- 依據(jù)一個非空值創(chuàng)建Optional
使得靜態(tài)工廠方法Optional.of,依據(jù)一個非空值創(chuàng)建一個Optional對象:
Optional<Car> optcar=Optional.of(car)
- 可接受null的Optional
使用靜態(tài)工廠方法Optional.ofNullable,你可以創(chuàng)建一個允許null值的Optional
對象:
Optional<Car> optCar = Optional.ofNullable(car);
如果car是null晤碘,那么得到的Optional對象就是個空對象愧哟。
completablerFuture組合式異步編程
-
future接口
它建模了一種異步計算,返回一個執(zhí)行運算結果的引用哼蛆,當運算結束后蕊梧,這個引用被返回給調(diào)用方。要使用Future腮介,通常你只需要將耗時的操作封裝在一個Callable對象中肥矢,再將它提交給ExecutorService,就萬事大吉了
使用CompletableFuture構建異步應用。
使用supplyAsync創(chuàng)建CompletableFuture對象甘改。
Java 8的 CompletableFuture API提供了名為thenCompose的方法旅东,它就是專門為這一目的而設計的,thenCompose方法允許你對兩個異步操作進行流水線十艾,第一個操作完成時抵代,將其結果作為參數(shù)傳遞給第二個操作。換句話說忘嫉,你可以創(chuàng)建兩個CompletableFutures對象荤牍,對第一個CompletableFuture對象調(diào)用thenCompose,并向其傳遞一個函數(shù)庆冕。當?shù)谝粋€CompletableFuture執(zhí)行完畢后康吵,它的結果將作為該函數(shù)的參數(shù),這個函數(shù)的返回值是以第一個CompletableFuture的返回做輸入計算出的第二個CompletableFuture對象访递。
CompletableFuture利用Lambda表達式以聲明式的API提供了一種機制晦嵌,能夠用最有效的方式,
非常容易地將多個以同步或異步方式執(zhí)行復雜操作的任務結合到一起
一等函數(shù)是可以作為參數(shù)傳遞拷姿,可以作為結果返回惭载,同時還能存儲在數(shù)據(jù)結構中的函數(shù)。
? 高階函數(shù)接受至少一個或者多個函數(shù)作為輸入?yún)?shù)响巢,或者返回另一個函數(shù)的函數(shù)描滔。Java
中典型的高階函數(shù)包括comparing、andThen和compose抵乓。
? 科里化是一種幫助你模塊化函數(shù)和重用代碼的技術伴挚。