




版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
\hJava8函數(shù)式編程目錄\h第1章簡(jiǎn)介\h1.1為什么需要再次修改Java\h1.2什么是函數(shù)式編程\h1.3示例\h第2章Lambda表達(dá)式\h2.1第一個(gè)Lambda表達(dá)式\h2.2如何辨別Lambda表達(dá)式\h2.3引用值,而不是變量\h2.4函數(shù)接口\h2.5類型推斷\h2.6要點(diǎn)回顧\h2.7練習(xí)\h第3章流\h3.1從外部迭代到內(nèi)部迭代\h3.2實(shí)現(xiàn)機(jī)制\h3.3常用的流操作\h3.3.1collect(toList())\h3.3.2map\h3.3.3filter\h3.3.4flatMap\h3.3.5max和min\h3.3.6通用模式\h3.3.7reduce\h3.3.8整合操作\h3.4重構(gòu)遺留代碼\h3.5多次調(diào)用流操作\h3.6高階函數(shù)\h3.7正確使用Lambda表達(dá)式\h3.8要點(diǎn)回顧\h3.9練習(xí)\h3.10進(jìn)階練習(xí)\h第4章類庫(kù)\h4.1在代碼中使用Lambda表達(dá)式\h4.2基本類型\h4.3重載解析\h4.4@FunctionalInterface\h4.5二進(jìn)制接口的兼容性\h4.6默認(rèn)方法\h默認(rèn)方法和子類\h4.7多重繼承\(zhòng)h三定律\h4.8權(quán)衡\h4.9接口的靜態(tài)方法\h4.10Optional\h4.11要點(diǎn)回顧\h4.12練習(xí)\h4.13開放練習(xí)\h第5章高級(jí)集合類和收集器\h5.1方法引用\h5.2元素順序\h5.3使用收集器\h5.3.1轉(zhuǎn)換成其他集合\h5.3.2轉(zhuǎn)換成值\h5.3.3數(shù)據(jù)分塊\h5.3.4數(shù)據(jù)分組\h5.3.5字符串\h5.3.6組合收集器\h5.3.7重構(gòu)和定制收集器\h5.3.8對(duì)收集器的歸一化處理\h5.4一些細(xì)節(jié)\h5.5要點(diǎn)回顧\h5.6練習(xí)\h第6章數(shù)據(jù)并行化\h6.1并行和并發(fā)\h6.2為什么并行化如此重要\h6.3并行化流操作\h6.4模擬系統(tǒng)\h6.5限制\h6.6性能\h6.7并行化數(shù)組操作\h6.8要點(diǎn)回顧\h6.9練習(xí)\h第7章測(cè)試、調(diào)試和重構(gòu)\h7.1重構(gòu)候選項(xiàng)\h7.1.1進(jìn)進(jìn)出出、搖搖晃晃\h7.1.2孤獨(dú)的覆蓋\h7.1.3同樣的東西寫兩遍\h7.2Lambda表達(dá)式的單元測(cè)試\h7.3在測(cè)試替身時(shí)使用Lambda表達(dá)式\h7.4惰性求值和調(diào)試\h7.5日志和打印消息\h7.6解決方案:peak\h7.7在流中間設(shè)置斷點(diǎn)\h7.8要點(diǎn)回顧\h第8章設(shè)計(jì)和架構(gòu)的原則\h8.1Lambda表達(dá)式改變了設(shè)計(jì)模式\h8.1.1命令者模式\h8.1.2策略模式\h8.1.3觀察者模式\h8.1.4模板方法模式\h8.2使用Lambda表達(dá)式的領(lǐng)域?qū)S谜Z(yǔ)言\h8.2.1使用Java編寫DSL\h8.2.2實(shí)現(xiàn)\h8.2.3評(píng)估\h8.3使用Lambda表達(dá)式的SOLID原則\h8.3.1單一功能原則\h8.3.2開閉原則\h8.3.3依賴反轉(zhuǎn)原則\h8.4進(jìn)階閱讀\h8.5要點(diǎn)回顧\h第9章使用Lambda表達(dá)式編寫并發(fā)程序\h9.1為什么要使用非阻塞式I/O\h9.2回調(diào)\h9.3消息傳遞架構(gòu)\h9.4末日金字塔\h9.5Future\h9.6CompletableFuture\h9.7響應(yīng)式編程\h9.8何時(shí)何地使用新技術(shù)\h9.9要點(diǎn)回顧\h9.10練習(xí)\h第10章下一步該怎么辦第1章簡(jiǎn)介在開始探索Lambda表達(dá)式之前,首先我們要知道它因何而生。本章將介紹Lambda表達(dá)式產(chǎn)生的原因,以及本書的寫作動(dòng)機(jī)和組織結(jié)構(gòu)。1.1為什么需要再次修改Java1996年1月,Java1.0發(fā)布,此后計(jì)算機(jī)編程領(lǐng)域發(fā)生了翻天覆地的變化。商業(yè)發(fā)展需要更復(fù)雜的應(yīng)用,大多數(shù)程序都跑在功能強(qiáng)大的多核CPU的機(jī)器上。帶有高效運(yùn)行時(shí)編譯器的Java虛擬機(jī)(JVM)的出現(xiàn),使程序員將更多精力放在編寫干凈、易于維護(hù)的代碼上,而不是思考如何將每一個(gè)CPU時(shí)鐘周期、每字節(jié)內(nèi)存物盡其用。多核CPU的興起成為了不容回避的事實(shí)。涉及鎖的編程算法不但容易出錯(cuò),而且耗費(fèi)時(shí)間。人們開發(fā)了java.util.concurrent包和很多第三方類庫(kù),試圖將并發(fā)抽象化,幫助程序員寫出在多核CPU上運(yùn)行良好的程序。很可惜,到目前為止,我們的成果還遠(yuǎn)遠(yuǎn)不夠。開發(fā)類庫(kù)的程序員使用Java時(shí),發(fā)現(xiàn)抽象級(jí)別還不夠。處理大型數(shù)據(jù)集合就是個(gè)很好的例子,面對(duì)大型數(shù)據(jù)集合,Java還欠缺高效的并行操作。開發(fā)者能夠使用Java8編寫復(fù)雜的集合處理算法,只需要簡(jiǎn)單修改一個(gè)方法,就能讓代碼在多核CPU上高效運(yùn)行。為了編寫這類處理批量數(shù)據(jù)的并行類庫(kù),需要在語(yǔ)言層面上修改現(xiàn)有的Java:增加Lambda表達(dá)式。當(dāng)然,這樣做是有代價(jià)的,程序員必須學(xué)習(xí)如何編寫和閱讀使用Lambda表達(dá)式的代碼,但是,這不是一樁賠本的買賣。與手寫一大段復(fù)雜、線程安全的代碼相比,學(xué)習(xí)一點(diǎn)新語(yǔ)法和一些新習(xí)慣容易很多。開發(fā)企業(yè)級(jí)應(yīng)用時(shí),好的類庫(kù)和框架極大地降低了開發(fā)時(shí)間和成本,也為開發(fā)易用且高效的類庫(kù)掃清了障礙。對(duì)于習(xí)慣了面向?qū)ο缶幊痰拈_發(fā)者來(lái)說(shuō),抽象的概念并不陌生。面向?qū)ο缶幊淌菍?duì)數(shù)據(jù)進(jìn)行抽象,而函數(shù)式編程是對(duì)行為進(jìn)行抽象。現(xiàn)實(shí)世界中,數(shù)據(jù)和行為并存,程序也是如此,因此這兩種編程方式我們都得學(xué)。這種新的抽象方式還有其他好處。不是所有人都在編寫性能優(yōu)先的代碼,對(duì)于這些人來(lái)說(shuō),函數(shù)式編程帶來(lái)的好處尤為明顯。程序員能編寫出更容易閱讀的代碼——這種代碼更多地表達(dá)了業(yè)務(wù)邏輯的意圖,而不是它的實(shí)現(xiàn)機(jī)制。易讀的代碼也易于維護(hù)、更可靠、更不容易出錯(cuò)。在寫回調(diào)函數(shù)和事件處理程序時(shí),程序員不必再糾纏于匿名內(nèi)部類的冗繁和可讀性,函數(shù)式編程讓事件處理系統(tǒng)變得更加簡(jiǎn)單。能將函數(shù)方便地傳遞也讓編寫惰性代碼變得容易,惰性代碼在真正需要時(shí)才初始化變量的值。Java8還讓集合類可以擁有一些額外的方法:default方法。程序員在維護(hù)自己的類庫(kù)時(shí),可以使用這些方法??偠灾?,Java已經(jīng)不是祖輩們當(dāng)年使用的Java了,嗯,這不是件壞事。1.2什么是函數(shù)式編程每個(gè)人對(duì)函數(shù)式編程的理解不盡相同。但其核心是:在思考問題時(shí),使用不可變值和函數(shù),函數(shù)對(duì)一個(gè)值進(jìn)行處理,映射成另一個(gè)值。不同的語(yǔ)言社區(qū)往往對(duì)各自語(yǔ)言中的特性孤芳自賞?,F(xiàn)在談Java程序員如何定義函數(shù)式編程還為時(shí)尚早,但是,這根本不重要!我們關(guān)心的是如何寫出好代碼,而不是符合函數(shù)式編程風(fēng)格的代碼。本書將重點(diǎn)放在函數(shù)式編程的實(shí)用性上,包括可以被大多數(shù)程序員理解和使用的技術(shù),幫助他們寫出易讀、易維護(hù)的代碼。1.3示例本書中的示例全部都圍繞一個(gè)常見的問題領(lǐng)域構(gòu)造:音樂。具體來(lái)說(shuō),這些示例代表了在專輯上常??吹降男畔ⅲ嘘P(guān)術(shù)語(yǔ)定義如下。Artist創(chuàng)作音樂的個(gè)人或團(tuán)隊(duì)。name:藝術(shù)家的名字(例如“甲殼蟲樂隊(duì)”)。members:樂隊(duì)成員(例如“約翰·列儂”),該字段可為空。origin:樂隊(duì)來(lái)自哪里(例如“利物浦”)。Track專輯中的一支曲目。name:曲目名稱(例如“黃色潛水艇”)。Album專輯,由若干曲目組成。name:專輯名(例如《左輪手槍》)。tracks:專輯上所有曲目的列表。musicians:參與創(chuàng)作本專輯的藝術(shù)家列表。本書將使用這個(gè)問題講解如何在正常的業(yè)務(wù)領(lǐng)域或者Java應(yīng)用中使用函數(shù)式編程技術(shù)。也許讀者認(rèn)為這些示例并不完美,但它和真實(shí)的業(yè)務(wù)領(lǐng)域應(yīng)用比起來(lái)足夠簡(jiǎn)單,書中的很多代碼都是基于這個(gè)簡(jiǎn)單的模型。第2章Lambda表達(dá)式Java8的最大變化是引入了Lambda表達(dá)式——一種緊湊的、傳遞行為的方式。它也是本書后續(xù)章節(jié)所述內(nèi)容的基礎(chǔ),因此,接下來(lái)就了解一下什么是Lambda表達(dá)式。2.1第一個(gè)Lambda表達(dá)式Swing是一個(gè)與平臺(tái)無(wú)關(guān)的Java類庫(kù),用來(lái)編寫圖形用戶界面(GUI)。該類庫(kù)有一個(gè)常見用法:為了響應(yīng)用戶操作,需要注冊(cè)一個(gè)事件監(jiān)聽器。用戶一輸入,監(jiān)聽器就會(huì)執(zhí)行一些操作(見例2-1)。例2-1使用匿名內(nèi)部類將行為和按鈕單擊進(jìn)行關(guān)聯(lián)button.addActionListener(newActionListener(){publicvoidactionPerformed(ActionEventevent){System.out.println("buttonclicked");}});在這個(gè)例子中,我們創(chuàng)建了一個(gè)新對(duì)象,它實(shí)現(xiàn)了ActionListener接口。這個(gè)接口只有一個(gè)方法actionPerformed,當(dāng)用戶點(diǎn)擊屏幕上的按鈕時(shí),button就會(huì)調(diào)用這個(gè)方法。匿名內(nèi)部類實(shí)現(xiàn)了該方法。在例2-1中該方法所執(zhí)行的只是輸出一條信息,表明按鈕已被點(diǎn)擊。這實(shí)際上是一個(gè)代碼即數(shù)據(jù)的例子——我們給按鈕傳遞了一個(gè)代表某種行為的對(duì)象。設(shè)計(jì)匿名內(nèi)部類的目的,就是為了方便Java程序員將代碼作為數(shù)據(jù)傳遞。不過,匿名內(nèi)部類還是不夠簡(jiǎn)便。為了調(diào)用一行重要的邏輯代碼,不得不加上4行冗繁的樣板代碼。若把樣板代碼用其他顏色區(qū)分開來(lái),就可一目了然:button.addActionListener(newActionListener(){publicvoidactionPerformed(ActionEventevent){System.out.println("buttonclicked");}});盡管如此,樣板代碼并不是唯一的問題:這些代碼還相當(dāng)難讀,因?yàn)樗鼪]有清楚地表達(dá)程序員的意圖。我們不想傳入對(duì)象,只想傳入行為。在Java8中,上述代碼可以寫成一個(gè)Lambda表達(dá)式,如例2-2所示。例2-2使用Lambda表達(dá)式將行為和按鈕單擊進(jìn)行關(guān)聯(lián)button.addActionListener(event->System.out.println("buttonclicked"));和傳入一個(gè)實(shí)現(xiàn)某接口的對(duì)象不同,我們傳入了一段代碼塊——一個(gè)沒有名字的函數(shù)。event是參數(shù)名,和上面匿名內(nèi)部類示例中的是同一個(gè)參數(shù)。->將參數(shù)和Lambda表達(dá)式的主體分開,而主體是用戶點(diǎn)擊按鈕時(shí)會(huì)運(yùn)行的一些代碼。和使用匿名內(nèi)部類的另一處不同在于聲明event參數(shù)的方式。使用匿名內(nèi)部類時(shí)需要顯式地聲明參數(shù)類型ActionEventevent,而在Lambda表達(dá)式中無(wú)需指定類型,程序依然可以編譯。這是因?yàn)閖avac根據(jù)程序的上下文(addActionListener方法的簽名)在后臺(tái)推斷出了參數(shù)event的類型。這意味著如果參數(shù)類型不言而明,則無(wú)需顯式指定。稍后會(huì)介紹類型推斷的更多細(xì)節(jié),現(xiàn)在先來(lái)看看編寫Lambda表達(dá)式的各種方式。盡管與之前相比,Lambda表達(dá)式中的參數(shù)需要的樣板代碼很少,但是Java8仍然是一種靜態(tài)類型語(yǔ)言。為了增加可讀性并遷就我們的習(xí)慣,聲明參數(shù)時(shí)也可以包括類型信息,而且有時(shí)編譯器不一定能根據(jù)上下文推斷出參數(shù)的類型!2.2如何辨別Lambda表達(dá)式Lambda表達(dá)式除了基本的形式之外,還有幾種變體,如例2-3所示。例2-3編寫Lambda表達(dá)式的不同形式RunnablenoArguments=()->System.out.println("HelloWorld");?ActionListeneroneArgument=event->System.out.println("buttonclicked");?RunnablemultiStatement=()->{?System.out.print("Hello");System.out.println("World");};BinaryOperator<Long>add=(x,y)->x+y;?BinaryOperator<Long>addExplicit=(Longx,Longy)->x+y;??中所示的Lambda表達(dá)式不包含參數(shù),使用空括號(hào)()表示沒有參數(shù)。該Lambda表達(dá)式實(shí)現(xiàn)了Runnable接口,該接口也只有一個(gè)run方法,沒有參數(shù),且返回類型為void。?中所示的Lambda表達(dá)式包含且只包含一個(gè)參數(shù),可省略參數(shù)的括號(hào),這和例2-2中的形式一樣。Lambda表達(dá)式的主體不僅可以是一個(gè)表達(dá)式,而且也可以是一段代碼塊,使用大括號(hào)({})將代碼塊括起來(lái),如?所示。該代碼塊和普通方法遵循的規(guī)則別無(wú)二致,可以用返回或拋出異常來(lái)退出。只有一行代碼的Lambda表達(dá)式也可使用大括號(hào),用以明確Lambda表達(dá)式從何處開始、到哪里結(jié)束。Lambda表達(dá)式也可以表示包含多個(gè)參數(shù)的方法,如?所示。這時(shí)就有必要思考怎樣去閱讀該Lambda表達(dá)式。這行代碼并不是將兩個(gè)數(shù)字相加,而是創(chuàng)建了一個(gè)函數(shù),用來(lái)計(jì)算兩個(gè)數(shù)字相加的結(jié)果。變量add的類型是BinaryOperator<Long>,它不是兩個(gè)數(shù)字的和,而是將兩個(gè)數(shù)字相加的那行代碼。到目前為止,所有Lambda表達(dá)式中的參數(shù)類型都是由編譯器推斷得出的。這當(dāng)然不錯(cuò),但有時(shí)最好也可以顯式聲明參數(shù)類型,此時(shí)就需要使用小括號(hào)將參數(shù)括起來(lái),多個(gè)參數(shù)的情況也是如此。如?所示。目標(biāo)類型是指Lambda表達(dá)式所在上下文環(huán)境的類型。比如,將Lambda表達(dá)式賦值給一個(gè)局部變量,或傳遞給一個(gè)方法作為參數(shù),局部變量或方法參數(shù)的類型就是Lambda表達(dá)式的目標(biāo)類型。上述例子還隱含了另外一層意思:Lambda表達(dá)式的類型依賴于上下文環(huán)境,是由編譯器推斷出來(lái)的。目標(biāo)類型也不是一個(gè)全新的概念。如例2-4所示,Java中初始化數(shù)組時(shí),數(shù)組的類型就是根據(jù)上下文推斷出來(lái)的。另一個(gè)常見的例子是null,只有將null賦值給一個(gè)變量,才能知道它的類型。例2-4等號(hào)右邊的代碼并沒有聲明類型,系統(tǒng)根據(jù)上下文推斷出類型信息finalString[]array={"hello","world"};2.3引用值,而不是變量如果你曾使用過匿名內(nèi)部類,也許遇到過這樣的情況:需要引用它所在方法里的變量。這時(shí),需要將變量聲明為final,如例2-5所示。將變量聲明為final,意味著不能為其重復(fù)賦值。同時(shí)也意味著在使用final變量時(shí),實(shí)際上是在使用賦給該變量的一個(gè)特定的值。例2-5匿名內(nèi)部類中使用final局部變量finalStringname=getUserName();button.addActionListener(newActionListener(){publicvoidactionPerformed(ActionEventevent){System.out.println("hi"+name);}});Java8雖然放松了這一限制,可以引用非final變量,但是該變量在既成事實(shí)上必須是final。雖然無(wú)需將變量聲明為final,但在Lambda表達(dá)式中,也無(wú)法用作非終態(tài)變量。如果堅(jiān)持用作非終態(tài)變量,編譯器就會(huì)報(bào)錯(cuò)。既成事實(shí)上的final是指只能給該變量賦值一次。換句話說(shuō),Lambda表達(dá)式引用的是值,而不是變量。在例2-6中,name就是一個(gè)既成事實(shí)上的final變量。例2-6Lambda表達(dá)式中引用既成事實(shí)上的final變量Stringname=getUserName();button.addActionListener(event->System.out.println("hi"+name));final就像代碼中的線路噪聲,省去之后代碼更易讀。當(dāng)然,有些情況下,顯式地使用final代碼更易懂。是否使用這種既成事實(shí)上的final變量,完全取決于個(gè)人喜好。如果你試圖給該變量多次賦值,然后在Lambda表達(dá)式中引用它,編譯器就會(huì)報(bào)錯(cuò)。比如,例2-7無(wú)法通過編譯,并顯示出錯(cuò)信息:localvariablesreferencedfromaLambdaexpressionmustbefinaloreffectivelyfinal1。1Lambda表達(dá)式中引用的局部變量必須是final或既成事實(shí)上的final變量?!g者注例2-7未使用既成事實(shí)上的final變量,導(dǎo)致無(wú)法通過編譯Stringname=getUserName();name=formatUserName(name);button.addActionListener(event->System.out.println("hi"+name));這種行為也解釋了為什么Lambda表達(dá)式也被稱為閉包。未賦值的變量與周邊環(huán)境隔離起來(lái),進(jìn)而被綁定到一個(gè)特定的值。在眾說(shuō)紛紜的計(jì)算機(jī)編程語(yǔ)言圈子里,Java是否擁有真正的閉包一直備受爭(zhēng)議,因?yàn)樵贘ava中只能引用既成事實(shí)上的final變量。名字雖異,功能相同,就好比把菠蘿叫作鳳梨,其實(shí)都是同一種水果。為了避免無(wú)意義的爭(zhēng)論,全書將使用“Lambda表達(dá)式”一詞。無(wú)論名字如何,如前文所述,Lambda表達(dá)式都是靜態(tài)類型的。因此,接下來(lái)就分析一下Lambda表達(dá)式本身的類型:函數(shù)接口。2.4函數(shù)接口函數(shù)接口是只有一個(gè)抽象方法的接口,用作Lambda表達(dá)式的類型。在Java里,所有方法參數(shù)都有固定的類型。假設(shè)將數(shù)字3作為參數(shù)傳給一個(gè)方法,則參數(shù)的類型是int。那么,Lambda表達(dá)式的類型又是什么呢?使用只有一個(gè)方法的接口來(lái)表示某特定方法并反復(fù)使用,是很早就有的習(xí)慣。使用Swing編寫過用戶界面的人對(duì)這種方式都不陌生,例2-2中的用法也是如此。這里無(wú)需再標(biāo)新立異,Lambda表達(dá)式也使用同樣的技巧,并將這種接口稱為函數(shù)接口。例2-8展示了前面例子中所用的函數(shù)接口。例2-8ActionListener接口:接受ActionEvent類型的參數(shù),返回空publicinterfaceActionListenerextendsEventListener{publicvoidactionPerformed(ActionEventevent);}ActionListener只有一個(gè)抽象方法:actionPerformed,被用來(lái)表示行為:接受一個(gè)參數(shù),返回空。記住,由于actionPerformed定義在一個(gè)接口里,因此abstract關(guān)鍵字不是必需的。該接口也繼承自一個(gè)不具有任何方法的父接口:EventListener。這就是函數(shù)接口,接口中單一方法的命名并不重要,只要方法簽名和Lambda表達(dá)式的類型匹配即可。可在函數(shù)接口中為參數(shù)起一個(gè)有意義的名字,增加代碼易讀性,便于更透徹地理解參數(shù)的用途。這里的函數(shù)接口接受一個(gè)ActionEvent類型的參數(shù),返回空(void),但函數(shù)接口還可有其他形式。例如,函數(shù)接口可以接受兩個(gè)參數(shù),并返回一個(gè)值,還可以使用泛型,這完全取決于你要干什么。以后我將使用圖形來(lái)表示不同類型的函數(shù)接口。指向函數(shù)接口的箭頭表示參數(shù),如果箭頭從函數(shù)接口射出,則表示方法的返回類型。ActionListener的函數(shù)接口如圖2-1所示。圖2-1:ActionListener接口,接受一個(gè)ActionEvent對(duì)象,返回空使用Java編程,總會(huì)遇到很多函數(shù)接口,但Java開發(fā)工具包(JDK)提供的一組核心函數(shù)接口會(huì)頻繁出現(xiàn)。表2-1羅列了一些最重要的函數(shù)接口。表2-1:Java中重要的函數(shù)接口接口參數(shù)返回類型示例Predicate<T>Tboolean這張唱片已經(jīng)發(fā)行了嗎Consumer<T>Tvoid輸出一個(gè)值Function<T,R>TR獲得Artist對(duì)象的名字Supplier<T>NoneT工廠方法UnaryOperator<T>TT邏輯非(!)BinaryOperator<T>(T,T)T求兩個(gè)數(shù)的乘積(*)前面已講過函數(shù)接口接收的類型,也講過javac可以根據(jù)上下文自動(dòng)推斷出參數(shù)的類型,且用戶也可以手動(dòng)聲明參數(shù)類型,但何時(shí)需要手動(dòng)聲明呢?下面將對(duì)類型推斷作詳盡說(shuō)明。2.5類型推斷某些情況下,用戶需要手動(dòng)指明類型,建議大家根據(jù)自己或項(xiàng)目組的習(xí)慣,采用讓代碼最便于閱讀的方法。有時(shí)省略類型信息可以減少干擾,更易弄清狀況;而有時(shí)卻需要類型信息幫助理解代碼。經(jīng)驗(yàn)證發(fā)現(xiàn),一開始類型信息是有用的,但隨后可以只在真正需要時(shí)才加上類型信息。下面將介紹一些簡(jiǎn)單的規(guī)則,來(lái)幫助確認(rèn)是否需要手動(dòng)聲明參數(shù)類型。Lambda表達(dá)式中的類型推斷,實(shí)際上是Java7就引入的目標(biāo)類型推斷的擴(kuò)展。讀者可能已經(jīng)知道Java7中的菱形操作符,它可使javac推斷出泛型參數(shù)的類型。參見例2-9。例2-9使用菱形操作符,根據(jù)變量類型做推斷Map<String,Integer>oldWordCounts=newHashMap<String,Integer>();?Map<String,Integer>diamondWordCounts=newHashMap<>();?我們?yōu)樽兞縪ldWordCounts?明確指定了泛型的類型,而變量diamondWordCounts?則使用了菱形操作符。不用明確聲明泛型類型,編譯器就可以自己推斷出來(lái),這就是它的神奇之處!當(dāng)然,這并不是什么魔法,根據(jù)變量diamondWordCounts?的類型可以推斷出HashMap的泛型類型,但用戶仍需要聲明變量的泛型類型。如果將構(gòu)造函數(shù)直接傳遞給一個(gè)方法,也可根據(jù)方法簽名來(lái)推斷類型。在例2-10中,我們傳入了HashMap,根據(jù)方法簽名已經(jīng)可以推斷出泛型的類型。例2-10使用菱形操作符,根據(jù)方法簽名做推斷useHashmap(newHashMap<>());...privatevoiduseHashmap(Map<String,String>values);Java7中程序員可省略構(gòu)造函數(shù)的泛型類型,Java8更進(jìn)一步,程序員可省略Lambda表達(dá)式中的所有參數(shù)類型。再?gòu)?qiáng)調(diào)一次,這并不是魔法,javac根據(jù)Lambda表達(dá)式上下文信息就能推斷出參數(shù)的正確類型。程序依然要經(jīng)過類型檢查來(lái)保證運(yùn)行的安全性,但不用再顯式聲明類型罷了。這就是所謂的類型推斷。Java8中對(duì)類型推斷系統(tǒng)的改善值得一提。上面的例子將newHashMap<>()傳給useHashmap方法,即使編譯器擁有足夠的信息,也無(wú)法在Java7中通過編譯。接下來(lái)將通過舉例來(lái)詳細(xì)分析類型推斷。例2-11和例2-12都將變量賦給一個(gè)函數(shù)接口,這樣便于理解。第一個(gè)例子(例2-11)使用Lambda表達(dá)式檢測(cè)一個(gè)Integer是否大于5。這實(shí)際上是一個(gè)Predicate——用來(lái)判斷真假的函數(shù)接口。例2-11類型推斷Predicate<Integer>atLeast5=x->x>5;Predicate也是一個(gè)Lambda表達(dá)式,和前文中ActionListener不同的是,它還返回一個(gè)值。在例2-11中,表達(dá)式x>5是Lambda表達(dá)式的主體。這樣的情況下,返回值就是Lambda表達(dá)式主體的值。例2-12Predicate接口的源碼,接受一個(gè)對(duì)象,返回一個(gè)布爾值publicinterfacePredicate<T>{booleantest(Tt);}從例2-12中可以看出,Predicate只有一個(gè)泛型類型的參數(shù),Integer用于其中。Lambda表達(dá)式實(shí)現(xiàn)了Predicate接口,因此它的單一參數(shù)被推斷為Integer類型。javac還可檢查L(zhǎng)ambda表達(dá)式的返回值是不是boolean,這正是Predicate方法的返回類型(如圖2-2)。圖2-2:Predicate接口圖示,接受一個(gè)對(duì)象,返回一個(gè)布爾值例2-13是一個(gè)略顯復(fù)雜的函數(shù)接口:BinaryOperator。該接口接受兩個(gè)參數(shù),返回一個(gè)值,參數(shù)和值的類型均相同。實(shí)例中所用的類型是Long。例2-13略顯復(fù)雜的類型推斷BinaryOperator<Long>addLongs=(x,y)->x+y;類型推斷系統(tǒng)相當(dāng)智能,但若信息不夠,類型推斷系統(tǒng)也無(wú)能為力。類型系統(tǒng)不會(huì)漫無(wú)邊際地瞎猜,而會(huì)中止操作并報(bào)告編譯錯(cuò)誤,尋求幫助。比如,如果我們刪掉例2-13中的某些類型信息,就會(huì)得到例2-14所示的代碼。例2-14沒有泛型,代碼則通不過編譯BinaryOperatoradd=(x,y)->x+y;編譯器給出的報(bào)錯(cuò)信息如下:Operator'+'cannotbeappliedtojava.lang.Object,java.lang.Object.報(bào)錯(cuò)信息讓人一頭霧水,到底怎么回事?BinaryOperator畢竟是一個(gè)具有泛型參數(shù)的函數(shù)接口,該類型既是參數(shù)x和y的類型,也是返回值的類型。上面的例子中并沒有給出變量add的任何泛型信息,給出的正是原始類型的定義。因此,編譯器認(rèn)為參數(shù)和返回值都是java.lang.Object實(shí)例。4.3節(jié)還會(huì)講到類型推斷,但就目前來(lái)說(shuō),掌握以上類型推斷的知識(shí)就已經(jīng)足夠了。2.6要點(diǎn)回顧Lambda表達(dá)式是一個(gè)匿名方法,將行為像數(shù)據(jù)一樣進(jìn)行傳遞。Lambda表達(dá)式的常見結(jié)構(gòu):BinaryOperator<Integer>add=(x,y)→x+y。函數(shù)接口指僅具有單個(gè)抽象方法的接口,用來(lái)表示Lambda表達(dá)式的類型。2.7練習(xí)每章最后都附有一組練習(xí),幫助讀者實(shí)踐并鞏固本章的知識(shí)和新概念。練習(xí)答案可在GitHub(\h/RichardWarburton/java-8-Lambdas-exercises)上本書所對(duì)應(yīng)的代碼倉(cāng)庫(kù)中找到。1.請(qǐng)看例2-15中的Function函數(shù)接口并回答下列問題。例2-15Function函數(shù)接口publicinterfaceFunction<T,R>{Rapply(Tt);}a.請(qǐng)畫出該函數(shù)接口的圖示。b.若要編寫一個(gè)計(jì)算器程序,你會(huì)使用該接口表示什么樣的Lambda表達(dá)式?c.下列哪些Lambda表達(dá)式有效實(shí)現(xiàn)了Function<Long,Long>?x->x+1;(x,y)->x+1;x->x==1;2.ThreadLocalLambda表達(dá)式。Java有一個(gè)ThreadLocal類,作為容器保存了當(dāng)前線程里局部變量的值。Java8為該類新加了一個(gè)工廠方法,接受一個(gè)Lambda表達(dá)式,并產(chǎn)生一個(gè)新的ThreadLocal對(duì)象,而不用使用繼承,語(yǔ)法上更加簡(jiǎn)潔。a.在Javadoc或集成開發(fā)環(huán)境(IDE)里找出該方法。b.DateFormatter類是非線程安全的。使用構(gòu)造函數(shù)創(chuàng)建一個(gè)線程安全的DateFormatter對(duì)象,并輸出日期,如“01-Jan-1970”。3.類型推斷規(guī)則。下面是將Lambda表達(dá)式作為參數(shù)傳遞給函數(shù)的一些例子。javac能正確推斷出Lambda表達(dá)式中參數(shù)的類型嗎?換句話說(shuō),程序能編譯嗎?a.RunnablehelloWorld=()->System.out.println("helloworld");b.使用Lambda表達(dá)式實(shí)現(xiàn)ActionListener接口:JButtonbutton=newJButton();button.addActionListener(event->System.out.println(event.getActionCommand()));c.以如下方式重載check方法后,還能正確推斷出check(x->x>5)的類型嗎?interfaceIntPred{booleantest(Integervalue);}booleancheck(Predicate<Integer>predicate);booleancheck(IntPredpredicate);你可能需要查閱Javadoc或在IDE里查看方法的參數(shù)類型,驗(yàn)證重載是否有效。第3章流Java8中新增的特性旨在幫助程序員寫出更好的代碼,其中對(duì)核心類庫(kù)的改進(jìn)是很關(guān)鍵的一部分,也是本章的主要內(nèi)容。對(duì)核心類庫(kù)的改進(jìn)主要包括集合類的API和新引入的流(Stream)。流使程序員得以站在更高的抽象層次上對(duì)集合進(jìn)行操作。本章會(huì)介紹Stream類中的一組方法,每個(gè)方法都對(duì)應(yīng)集合上的一種操作。3.1從外部迭代到內(nèi)部迭代本章及本書其余部分的例子大多圍繞1.3節(jié)介紹的案例展開。Java程序員在使用集合類時(shí),一個(gè)通用的模式是在集合上進(jìn)行迭代,然后處理返回的每一個(gè)元素。比如要計(jì)算從倫敦來(lái)的藝術(shù)家的人數(shù),通常代碼會(huì)寫成例3-1這樣。例3-1使用for循環(huán)計(jì)算來(lái)自倫敦的藝術(shù)家人數(shù)intcount=0;for(Artistartist:allArtists){if(artist.isFrom("London")){count++;}}盡管這樣的操作可行,但存在幾個(gè)問題。每次迭代集合類時(shí),都需要寫很多樣板代碼。將for循環(huán)改造成并行方式運(yùn)行也很麻煩,需要修改每個(gè)for循環(huán)才能實(shí)現(xiàn)。此外,上述代碼無(wú)法流暢傳達(dá)程序員的意圖。for循環(huán)的樣板代碼模糊了代碼的本意,程序員必須閱讀整個(gè)循環(huán)體才能理解。若是單一的for循環(huán),倒也問題不大,但面對(duì)一個(gè)滿是循環(huán)(尤其是嵌套循環(huán))的龐大代碼庫(kù)時(shí),負(fù)擔(dān)就重了。就其背后的原理來(lái)看,for循環(huán)其實(shí)是一個(gè)封裝了迭代的語(yǔ)法糖,我們?cè)谶@里多花點(diǎn)時(shí)間,看看它的工作原理。首先調(diào)用iterator方法,產(chǎn)生一個(gè)新的Iterator對(duì)象,進(jìn)而控制整個(gè)迭代過程,這就是外部迭代。迭代過程通過顯式調(diào)用Iterator對(duì)象的hasNext和next方法完成迭代。展開后的代碼如例3-2所示,圖3-1展示了迭代過程中的方法調(diào)用。例3-2使用迭代器計(jì)算來(lái)自倫敦的藝術(shù)家人數(shù)intcount=0;Iterator<Artist>iterator=allArtists.iterator();while(iterator.hasNext()){Artistartist=iterator.next();if(artist.isFrom("London")){count++;}}圖3-1:外部迭代然而,外部迭代也有問題。首先,它很難抽象出本章稍后提及的不同操作;此外,它從本質(zhì)上來(lái)講是一種串行化操作??傮w來(lái)看,使用for循環(huán)會(huì)將行為和方法混為一談。另一種方法就是內(nèi)部迭代,如例3-3所示。首先要注意stream()方法的調(diào)用,它和例3-2中調(diào)用iterator()的作用一樣。該方法不是返回一個(gè)控制迭代的Iterator對(duì)象,而是返回內(nèi)部迭代中的相應(yīng)接口:Stream。例3-3使用內(nèi)部迭代計(jì)算來(lái)自倫敦的藝術(shù)家人數(shù)longcount=allArtists.stream().filter(artist->artist.isFrom("London")).count();圖3-2展示了使用類庫(kù)后的方法調(diào)用流程,與圖3-1形成對(duì)比。圖3-2:內(nèi)部迭代Stream是用函數(shù)式編程方式在集合類上進(jìn)行復(fù)雜操作的工具。例3-3可被分解為兩步更簡(jiǎn)單的操作:找出所有來(lái)自倫敦的藝術(shù)家;計(jì)算他們的人數(shù)。每種操作都對(duì)應(yīng)Stream接口的一個(gè)方法。為了找出來(lái)自倫敦的藝術(shù)家,需要對(duì)Stream對(duì)象進(jìn)行過濾:filter。過濾在這里是指“只保留通過某項(xiàng)測(cè)試的對(duì)象”。測(cè)試由一個(gè)函數(shù)完成,根據(jù)藝術(shù)家是否來(lái)自倫敦,該函數(shù)返回true或者false。由于StreamAPI的函數(shù)式編程風(fēng)格,我們并沒有改變集合的內(nèi)容,而是描述出Stream里的內(nèi)容。count()方法計(jì)算給定Stream里包含多少個(gè)對(duì)象。3.2實(shí)現(xiàn)機(jī)制例3-3中,整個(gè)過程被分解為兩種更簡(jiǎn)單的操作:過濾和計(jì)數(shù),看似有化簡(jiǎn)為繁之嫌——例3-1中只含一個(gè)for循環(huán),兩種操作是否意味著需要兩次循環(huán)?事實(shí)上,類庫(kù)設(shè)計(jì)精妙,只需對(duì)藝術(shù)家列表迭代一次。通常,在Java中調(diào)用一個(gè)方法,計(jì)算機(jī)會(huì)隨即執(zhí)行操作:比如,System.out.println("HelloWorld");會(huì)在終端上輸出一條信息。Stream里的一些方法卻略有不同,它們雖是普通的Java方法,但返回的Stream對(duì)象卻不是一個(gè)新集合,而是創(chuàng)建新集合的配方。現(xiàn)在,嘗試思考一下例3-4中代碼的作用,一時(shí)毫無(wú)頭緒也沒關(guān)系,稍后會(huì)詳細(xì)解釋。例3-4只過濾,不計(jì)數(shù)allArtists.stream().filter(artist->artist.isFrom("London"));這行代碼并未做什么實(shí)際性的工作,filter只刻畫出了Stream,但沒有產(chǎn)生新的集合。像filter這樣只描述Stream,最終不產(chǎn)生新集合的方法叫作惰性求值方法;而像count這樣最終會(huì)從Stream產(chǎn)生值的方法叫作及早求值方法。如果在過濾器中加入一條println語(yǔ)句,來(lái)輸出藝術(shù)家的名字,就能輕而易舉地看出其中的不同。例3-5對(duì)例3-4作了一些修改,加入了輸出語(yǔ)句。運(yùn)行這段代碼,程序不會(huì)輸出任何信息!例3-5由于使用了惰性求值,沒有輸出藝術(shù)家的名字allArtists.stream().filter(artist->{System.out.println(artist.getName());returnartist.isFrom("London");});如果將同樣的輸出語(yǔ)句加入一個(gè)擁有終止操作的流,如例3-3中的計(jì)數(shù)操作,藝術(shù)家的名字就會(huì)被輸出(見例3-6)。例3-6輸出藝術(shù)家的名字longcount=allArtists.stream().filter(artist->{System.out.println(artist.getName());returnartist.isFrom("London");}).count();以披頭士樂隊(duì)的成員作為藝術(shù)家列表,運(yùn)行上述程序,命令行里輸出的內(nèi)容如例3-7所示。例3-7顯示披頭士樂隊(duì)成員名單的示例輸出JohnLennonPaulMcCartneyGeorgeHarrisonRingoStarr判斷一個(gè)操作是惰性求值還是及早求值很簡(jiǎn)單:只需看它的返回值。如果返回值是Stream,那么是惰性求值;如果返回值是另一個(gè)值或?yàn)榭?,那么就是及早求值。使用這些操作的理想方式就是形成一個(gè)惰性求值的鏈,最后用一個(gè)及早求值的操作返回想要的結(jié)果,這正是它的合理之處。計(jì)數(shù)的示例也是這樣運(yùn)行的,但這只是最簡(jiǎn)單的情況:只含兩步操作。整個(gè)過程和建造者模式有共通之處。建造者模式使用一系列操作設(shè)置屬性和配置,最后調(diào)用一個(gè)build方法,這時(shí),對(duì)象才被真正創(chuàng)建。讀者一定會(huì)問:“為什么要區(qū)分惰性求值和及早求值?”只有在對(duì)需要什么樣的結(jié)果和操作有了更多了解之后,才能更有效率地進(jìn)行計(jì)算。例如,如果要找出大于10的第一個(gè)數(shù)字,那么并不需要和所有元素去做比較,只要找出第一個(gè)匹配的元素就夠了。這也意味著可以在集合類上級(jí)聯(lián)多種操作,但迭代只需一次。3.3常用的流操作為了更好地理解StreamAPI,掌握一些常用的Stream操作十分必要。除此處講述的幾種重要操作之外,該API的Javadoc中還有更多信息。3.3.1collect(toList())collect(toList())方法由Stream里的值生成一個(gè)列表,是一個(gè)及早求值操作。Stream的of方法使用一組初始值生成新的Stream。事實(shí)上,collect的用法不僅限于此,它是一個(gè)非常通用的強(qiáng)大結(jié)構(gòu),第5章將詳細(xì)介紹它的其他用途。下面是使用collect方法的一個(gè)例子:List<String>collected=Stream.of("a","b","c")?.collect(Collectors.toList());?assertEquals(Arrays.asList("a","b","c"),collected);?這段程序展示了如何使用collect(toList())方法從Stream中生成一個(gè)列表。如上文所述,由于很多Stream操作都是惰性求值,因此調(diào)用Stream上一系列方法之后,還需要最后再調(diào)用一個(gè)類似collect的及早求值方法。這個(gè)例子也展示了本節(jié)中所有示例代碼的通用格式。首先由列表生成一個(gè)Stream?,然后進(jìn)行一些Stream上的操作,繼而是collect操作,由Stream生成列表?,最后使用斷言判斷結(jié)果是否和預(yù)期一致?。形象一點(diǎn)兒的話,可以將Stream想象成漢堡,將最前和最后對(duì)Stream操作的方法想象成兩片面包,這兩片面包幫助我們認(rèn)清操作的起點(diǎn)和終點(diǎn)。3.3.2map如果有一個(gè)函數(shù)可以將一種類型的值轉(zhuǎn)換成另外一種類型,map操作就可以使用該函數(shù),將一個(gè)流中的值轉(zhuǎn)換成一個(gè)新的流。讀者可能已經(jīng)注意到,以前編程時(shí)或多或少使用過類似map的操作。比如編寫一段Java代碼,將一組字符串轉(zhuǎn)換成對(duì)應(yīng)的大寫形式。在一個(gè)循環(huán)中,對(duì)每個(gè)字符串調(diào)用toUppercase方法,然后將得到的結(jié)果加入一個(gè)新的列表。代碼如例3-8所示。例3-8使用for循環(huán)將字符串轉(zhuǎn)換為大寫List<String>collected=newArrayList<>();for(Stringstring:asList("a","b","hello")){StringuppercaseString=string.toUpperCase();collected.add(uppercaseString);}assertEquals(asList("A","B","HELLO"),collected);如果你經(jīng)常實(shí)現(xiàn)例3-8中這樣的for循環(huán),就不難猜出map是Stream上最常用的操作之一(如圖3-3所示)。例3-9展示了如何使用新的流框架將一組字符串轉(zhuǎn)換成大寫形式。圖3-3:map操作例3-9使用map操作將字符串轉(zhuǎn)換為大寫形式List<String>collected=Stream.of("a","b","hello").map(string->string.toUpperCase())?.collect(toList());assertEquals(asList("A","B","HELLO"),collected);傳給map?的Lambda表達(dá)式只接受一個(gè)String類型的參數(shù),返回一個(gè)新的String。參數(shù)和返回值不必屬于同一種類型,但是Lambda表達(dá)式必須是Function接口的一個(gè)實(shí)例(如圖3-4所示),F(xiàn)unction接口是只包含一個(gè)參數(shù)的普通函數(shù)接口。圖3-4:Function接口3.3.3filter遍歷數(shù)據(jù)并檢查其中的元素時(shí),可嘗試使用Stream中提供的新方法filter(如圖3-5所示)。圖3-5:filter操作上面就是一個(gè)使用filter的例子,如果你已熟悉這一概念,也可以選擇跳過本節(jié)。啊哈!您還沒跳過本節(jié)?那太好了,我們一起來(lái)看看這個(gè)方法有什么用。假設(shè)要找出一組字符串中以數(shù)字開頭的字符串,比如字符串"1abc"和"abc",其中"1abc"就是符合條件的字符串。可以使用一個(gè)for循環(huán),內(nèi)部用if條件語(yǔ)句判斷字符串的第一個(gè)字符來(lái)解決這個(gè)問題,代碼如例3-10所示。例3-10使用循環(huán)遍歷列表,使用條件語(yǔ)句做判斷List<String>beginningWithNumbers=newArrayList<>();for(Stringvalue:asList("a","1abc","abc1")){if(isDigit(value.charAt(0))){beginningWithNumbers.add(value);}}assertEquals(asList("1abc"),beginningWithNumbers);你可能已經(jīng)寫過很多類似的代碼:這被稱為filter模式。該模式的核心思想是保留Stream中的一些元素,而過濾掉其他的。例3-11展示了如何使用函數(shù)式風(fēng)格編寫相同的代碼。例3-11函數(shù)式風(fēng)格List<String>beginningWithNumbers=Stream.of("a","1abc","abc1").filter(value->isDigit(value.charAt(0))).collect(toList());assertEquals(asList("1abc"),beginningWithNumbers);和map很像,filter接受一個(gè)函數(shù)作為參數(shù),該函數(shù)用Lambda表達(dá)式表示。該函數(shù)和前面示例中if條件判斷語(yǔ)句的功能一樣,如果字符串首字母為數(shù)字,則返回true。若要重構(gòu)遺留代碼,for循環(huán)中的if條件語(yǔ)句就是一個(gè)很強(qiáng)的信號(hào),可用filter方法替代。由于此方法和if條件語(yǔ)句的功能相同,因此其返回值肯定是true或者false。經(jīng)過過濾,Stream中符合條件的,即Lambda表達(dá)式值為true的元素被保留下來(lái)。該Lambda表達(dá)式的函數(shù)接口正是前面章節(jié)中介紹過的Predicate(如圖3-6所示)。圖3-6:Predicate接口3.3.4flatMapflatMap方法可用Stream替換值,然后將多個(gè)Stream連接成一個(gè)Stream(如圖3-7所示)。圖3-7:flatMap操作前面已介紹過map操作,它可用一個(gè)新的值代替Stream中的值。但有時(shí),用戶希望讓map操作有點(diǎn)變化,生成一個(gè)新的Stream對(duì)象取而代之。用戶通常不希望結(jié)果是一連串的流,此時(shí)flatMap最能派上用場(chǎng)。我們看一個(gè)簡(jiǎn)單的例子。假設(shè)有一個(gè)包含多個(gè)列表的流,現(xiàn)在希望得到所有數(shù)字的序列。該問題的一個(gè)解法如例3-12所示。例3-12包含多個(gè)列表的StreamList<Integer>together=Stream.of(asList(1,2),asList(3,4)).flatMap(numbers->numbers.stream()).collect(toList());assertEquals(asList(1,2,3,4),together);調(diào)用stream方法,將每個(gè)列表轉(zhuǎn)換成Stream對(duì)象,其余部分由flatMap方法處理。flatMap方法的相關(guān)函數(shù)接口和map方法的一樣,都是Function接口,只是方法的返回值限定為Stream類型罷了。3.3.5max和minStream上常用的操作之一是求最大值和最小值。StreamAPI中的max和min操作足以解決這一問題。例3-13是查找專輯中最短曲目所用的代碼,展示了如何使用max和min操作。為了方便檢查程序結(jié)果是否正確,代碼片段中羅列了專輯中的曲目信息,我承認(rèn),這張專輯是有點(diǎn)冷門。例3-13使用Stream查找最短曲目List<Track>tracks=asList(newTrack("Bakai",524),newTrack("VioletsforYourFurs",378),newTrack("TimeWas",451));TrackshortestTrack=tracks.stream().min(Cparing(track->track.getLength())).get();assertEquals(tracks.get(1),shortestTrack);查找Stream中的最大或最小元素,首先要考慮的是用什么作為排序的指標(biāo)。以查找專輯中的最短曲目為例,排序的指標(biāo)就是曲目的長(zhǎng)度。為了讓Stream對(duì)象按照曲目長(zhǎng)度進(jìn)行排序,需要傳給它一個(gè)Comparator對(duì)象。Java8提供了一個(gè)新的靜態(tài)方法comparing,使用它可以方便地實(shí)現(xiàn)一個(gè)比較器。放在以前,我們需要比較兩個(gè)對(duì)象的某項(xiàng)屬性的值,現(xiàn)在只需要提供一個(gè)存取方法就夠了。本例中使用getLength方法。花點(diǎn)時(shí)間研究一下comparing方法是值得的。實(shí)際上這個(gè)方法接受一個(gè)函數(shù)并返回另一個(gè)函數(shù)。我知道,這聽起來(lái)像句廢話,但是卻很有用。這個(gè)方法本該早已加入Java標(biāo)準(zhǔn)庫(kù),但由于匿名內(nèi)部類可讀性差且書寫冗長(zhǎng),一直未能實(shí)現(xiàn)。現(xiàn)在有了Lambda表達(dá)式,代碼變得簡(jiǎn)潔易懂。此外,還可以調(diào)用空Stream的max方法,返回Optional對(duì)象。Optional對(duì)象有點(diǎn)陌生,它代表一個(gè)可能存在也可能不存在的值。如果Stream為空,那么該值不存在,如果不為空,則該值存在。先不必細(xì)究,4.10節(jié)將詳細(xì)講述Optional對(duì)象,現(xiàn)在唯一需要記住的是,通過調(diào)用get方法可以取出Optional對(duì)象中的值。3.3.6通用模式max和min方法都屬于更通用的一種編程模式。要看到這種編程模式,最簡(jiǎn)單的方法是使用for循環(huán)重寫例3-13中的代碼。例3-14和例3-13的功能一樣,都是查找專輯中的最短曲目,但是使用了for循環(huán)。例3-14使用for循環(huán)查找最短曲目List<Track>tracks=asList(newTrack("Bakai",524),newTrack("VioletsforYourFurs",378),newTrack("TimeWas",451));TrackshortestTrack=tracks.get(0);for(Tracktrack:tracks){if(track.getLength()<shortestTrack.getLength()){shortestTrack=track;}}assertEquals(tracks.get(1),shortestTrack);這段代碼先使用列表中的第一個(gè)元素初始化變量shortestTrack,然后遍歷曲目列表,如果找到更短的曲目,則更新shortestTrack,最后變量shortestTrack保存的正是最短曲目。程序員們無(wú)疑已寫過成千上萬(wàn)次這樣的for循環(huán),其中很多都屬于這個(gè)模式。例3-15中的偽代碼體現(xiàn)了通用模式的特點(diǎn)。例3-15reduce模式Objectaccumulator=initialValue;for(Objectelement:collection){accumulator=combine(accumulator,element);}首先賦給accumulator一個(gè)初始值:initialValue,然后在循環(huán)體中,通過調(diào)用combine函數(shù),拿accumulator和集合中的每一個(gè)元素做運(yùn)算,再將運(yùn)算結(jié)果賦給accumulator,最后accumulator的值就是想要的結(jié)果。這個(gè)模式中的兩個(gè)可變項(xiàng)是initialValue初始值和combine函數(shù)。在例3-14中,我們選列表中的第一個(gè)元素為初始值,但也不必需如此。為了找出最短曲目,combine函數(shù)返回當(dāng)前元素和accumulator中較短的那個(gè)。接下來(lái)看一下StreamAPI中的reduce操作是怎么工作的。3.3.7reducereduce操作可以實(shí)現(xiàn)從一組值中生成一個(gè)值。在上述例子中用到的count、min和max方法,因?yàn)槌S枚患{入標(biāo)準(zhǔn)庫(kù)中。事實(shí)上,這些方法都是reduce操作。圖3-8展示了如何通過reduce操作對(duì)Stream中的數(shù)字求和。以0作起點(diǎn)——一個(gè)空Stream的求和結(jié)果,每一步都將Stream中的元素累加至accumulator,遍歷至Stream中的最后一個(gè)元素時(shí),accumulator的值就是所有元素的和。圖3-8使用reduce操作實(shí)現(xiàn)累加例3-16中的代碼展示了這一過程。Lambda表達(dá)式就是reducer,它執(zhí)行求和操作,有兩個(gè)參數(shù):傳入Stream中的當(dāng)前元素和acc。將兩個(gè)參數(shù)相加,acc是累加器,保存著當(dāng)前的累加結(jié)果。例3-16使用reduce求和intcount=Stream.of(1,2,3).reduce(0,(acc,element)->acc+element);assertEquals(6,count);Lambda表達(dá)式的返回值是最新的acc,是上一輪acc的值和當(dāng)前元素相加的結(jié)果。reducer的類型是第2章已介紹過的BinaryOperator。4.2節(jié)將介紹另外一種標(biāo)準(zhǔn)類庫(kù)內(nèi)置的求和方法,在實(shí)際生產(chǎn)環(huán)境中,應(yīng)該使用那種方式,而不是使用像上面這個(gè)例子中的代碼。表3-1顯示了求和過程中的中間值。事實(shí)上,可以將reduce操作展開,得到例3-17這樣形式的代碼。例3-17展開reduce操作BinaryOperator<Integer>accumulator=(acc,element)->acc+element;intcount=accumulator.apply(accumulator.apply(accumulator.apply(0,1),2),3);表3-1reduce過程的中間值元素acc結(jié)果N/AN/A0101213336例3-18是可實(shí)現(xiàn)同樣功能的命令式Java代碼,從中可清楚看出函數(shù)式編程和命令式編程的區(qū)別。例3-18使用命令式編程方式求和intacc=0;for(Integerelement:asList(1,2,3)){acc=acc+element;}assertEquals(6,acc);在命令式編程方式下,每一次循環(huán)將集合中的元素和累加器相加,用相加后的結(jié)果更新累加器的值。對(duì)于集合來(lái)說(shuō),循環(huán)在外部,且需要手動(dòng)更新變量。3.3.8整合操作Stream接口的方法如此之多,有時(shí)會(huì)讓人難以選擇,像闖入一個(gè)迷宮,不知道該用哪個(gè)方法更好。本節(jié)將舉例說(shuō)明如何將問題分解為簡(jiǎn)單的Stream操作。第一個(gè)要解決的問題是,找出某張專輯上所有樂隊(duì)的國(guó)籍。藝術(shù)家列表里既有個(gè)人,也有樂隊(duì)。利用一點(diǎn)領(lǐng)域知識(shí),假定一般樂隊(duì)名以定冠詞The開頭。當(dāng)然這不是絕對(duì)的,但也差不多。需要注意的是,這個(gè)問題絕不是簡(jiǎn)單地調(diào)用幾個(gè)API就足以解決。這既不是使用map將一組值映射為另一組值,也不是過濾,更不是將Stream中的元素最終歸約為一個(gè)值。首先,可將這個(gè)問題分解為如下幾個(gè)步驟。1.找出專輯上的所有表演者。2.分辨出哪些表演者是樂隊(duì)。3.找出每個(gè)樂隊(duì)的國(guó)籍。4.將找出的國(guó)籍放入一個(gè)集合。現(xiàn)在,找出每一步對(duì)應(yīng)的StreamAPI就相對(duì)容易了:1.Album類有個(gè)getMusicians方法,該方法返回一個(gè)Stream對(duì)象,包含整張專輯中所有的表演者;2.使用filter方法對(duì)表演者進(jìn)行過濾,只保留樂隊(duì);3.使用map方法將樂隊(duì)映射為其所屬國(guó)家;4.使用collect(Collectors.toList())方法將國(guó)籍放入一個(gè)列表。最后,整合所有的操作,就得到如下代碼:Set<String>origins=album.getMusicians().filter(artist->artist.getName().startsWith("The")).map(artist->artist.getNationality()).collect(toSet());這個(gè)例子將Stream的鏈?zhǔn)讲僮髡宫F(xiàn)得淋漓盡致,調(diào)用getMusicians、filter和map方法都返回Stream對(duì)象,因此都屬于惰性求值,而collect方法屬于及早求值。map方法接受一個(gè)Lambda表達(dá)式,使用該Lambda表達(dá)式對(duì)Stream上的每個(gè)元素做映射,形成一個(gè)新的Stream。這個(gè)問題處理起來(lái)很方便,使用getMusicians方法獲取專輯上的藝術(shù)家列表時(shí)得到的是一個(gè)Stream對(duì)象。然而,處理其他實(shí)際遇到的問題時(shí)未必也能如此方便,很可能沒有方法可以返回一個(gè)Stream對(duì)象,反而得到像List或Set這樣的集合類。別擔(dān)心,只要調(diào)用List或Set的stream方法就能得到一個(gè)Stream對(duì)象。現(xiàn)在或許是個(gè)思考的好機(jī)會(huì),你真的需要對(duì)外暴露一個(gè)List或Set對(duì)象嗎?可能一個(gè)Stream工廠才是更好的選擇。通過Stream暴露集合的最大優(yōu)點(diǎn)在于,它很好地封裝了內(nèi)部實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu)。僅暴露一個(gè)Stream接口,用戶在實(shí)際操作中無(wú)論如何使用,都不會(huì)影響內(nèi)部的List或Set。同時(shí)這也鼓勵(lì)用戶在編程中使用更現(xiàn)代的Java8風(fēng)格。不必一蹴而就,可以對(duì)已有代碼漸進(jìn)性地重構(gòu),保留原有的取值函數(shù),添加返回Stream對(duì)象的函數(shù),時(shí)間長(zhǎng)了,就可以刪掉所有返回List或Set的取值函數(shù)。清理了所有遺留代碼之后,這種重構(gòu)方式讓人感覺棒極了!3.4重構(gòu)遺留代碼為了進(jìn)一步闡釋如何重構(gòu)遺留代碼,本節(jié)將舉例說(shuō)明如何將一段使用循環(huán)進(jìn)行集合操作的代碼,重構(gòu)成基于Stream的操作。重構(gòu)過程中的每一步都能確保代碼通過單元測(cè)試,當(dāng)然你也可以自行實(shí)際操作一遍,體驗(yàn)并驗(yàn)證。假定選定一組專輯,找出其中所有長(zhǎng)度大于1分鐘的曲目名稱。例3-19是遺留代碼,首先初始化一個(gè)Set對(duì)象,用來(lái)保存找到的曲目名稱。然后使用for循環(huán)遍歷所有專輯,每次循環(huán)中再使用一個(gè)for循環(huán)遍歷每張專輯上的每首曲目,檢查其長(zhǎng)度是否大于60秒,如果是,則將該曲目名稱加入Set對(duì)象。例3-19遺留代碼:找出長(zhǎng)度大于1分鐘的曲目publicSet<String>findLongTracks(List<Album>albums){Set<String>trackNames=newHashSet<>();for(Albumalbum:albums){for(Tracktrack:album.getTrackList()){if(track.getLength()>60){Stringname=track.getName();trackNames.add(name);}}}returntrackNames;}如果仔細(xì)閱讀上面的這段代碼,就會(huì)發(fā)現(xiàn)幾組嵌套的循環(huán)。僅通過閱讀這段代碼很難看出它的編寫目的,那就來(lái)重構(gòu)一下(使用流來(lái)重構(gòu)該段代碼的方式很多,下面介紹的只是其中一種。事實(shí)上,對(duì)StreamAPI越熟悉,就越不需要細(xì)分步驟。之所以在示例中一步一步地重構(gòu),完全是出于幫助大家學(xué)習(xí)的目的,在工作中無(wú)需這樣做)。第一步要修改的是for循環(huán)。首先使用Stream的forEach方法替換掉for循環(huán),但還是暫時(shí)保留原來(lái)循環(huán)體中的代碼,這是在重構(gòu)時(shí)非常方便的一個(gè)技巧。調(diào)用stream方法從專輯列表中生成第一個(gè)Stream,同時(shí)不要忘了在上一節(jié)已介紹過,getTracks方法本身就返回一個(gè)Stream對(duì)象。經(jīng)過第一步重構(gòu)后,代碼如例3-20所示。例3-20重構(gòu)的第一步:找出長(zhǎng)度大于1分鐘的曲目publicSet<String>findLongTracks(List<Album>albums){Set<String>trackNames=newHashSet<>();albums.stream().forEach(album->{album.getTracks().forEach(track->{if(track.getLength()>60){Stringname=track.getName();trackNames.add(name);}});});returntrackNames;}在重構(gòu)的第一步中,雖然使用了流,但是并沒有充分發(fā)揮它的作用。事實(shí)上,重構(gòu)后的代碼還不如原來(lái)的代碼好——天哪!因此,是時(shí)候引入一些更符合流風(fēng)格的代碼了,最內(nèi)層的forEach方法正是主要突破口。最內(nèi)層的forEach方法有三個(gè)功用:找出長(zhǎng)度大于1分鐘的曲目,得到符合條件的曲目名稱,將曲目名稱加入集合Set。這就意味著需要三項(xiàng)Stream操作:找出滿足某種條件的曲目是filter的功能,得到曲目名稱則可用map達(dá)成,終結(jié)操作可使用forEach方法將曲目名稱加入一個(gè)集合。用以上三項(xiàng)Stream操作將內(nèi)部的forEach方法拆分后,代碼如例3-21所示。例3-21重構(gòu)的第二步:找出長(zhǎng)度大于1分鐘的曲目publicSet<String>findLongTracks(List<Album>albums){Set<String>trackNames=newHashSet<>();albums.stream().forEach(album->{album.getTracks().filter(track->track.getLength()>60).map(track->track.getName()).forEach(name->trackNames.add(name));});returntrackNames;}現(xiàn)在用更符合流風(fēng)格的操作替換了內(nèi)層的循環(huán),但代碼看起來(lái)還是冗長(zhǎng)繁瑣。將各種流嵌套起來(lái)并不理想,最好還是用干凈整潔的順序調(diào)用一些方法。理想的操作莫過于找到一種方法,將專輯轉(zhuǎn)化成一個(gè)曲目的Stream。眾所周知,任何時(shí)候想轉(zhuǎn)化或替代代碼,都該使用map操作。這里將使用比map更復(fù)雜的flatMap操作,把多個(gè)Stream合并成一個(gè)Stream并返回。將forEach方法替換成flatMap后,代碼如例3-22所示。例3-22重構(gòu)的第三步:找出長(zhǎng)度大于1分鐘的曲目publicSet<String>findLongTracks(List<Album>albums){Set<String>trackNames=newHashSet<>();albums.stream().flatMap(album->album.getTracks()).filter(track->track.getLength()>60).map(track->track.getName()).forEach(name->trackNames.add(name));returntrackNames;}上面的代碼中使用一組簡(jiǎn)潔的方法調(diào)用替換掉兩個(gè)嵌套的for循環(huán),看起來(lái)清晰很多。然而至此并未結(jié)束,仍需手動(dòng)創(chuàng)建一個(gè)Set對(duì)象并將元素加入其中,但我們希望看到的是整個(gè)計(jì)算任務(wù)由一連串的Stream操作完成。到目前為止,雖然還未展示轉(zhuǎn)換的方法,但已有類似的操作。就像使用collect(Collectors.toList())可以將Stream中的值轉(zhuǎn)換成一個(gè)列表,使用collect(Collectors.toSet())可以將Stream中的值轉(zhuǎn)換成一個(gè)集合。因此,將最后的forEach方法替換為collect,并刪掉變量trackNames,代碼如例3-23所示。例3-23重構(gòu)的第四步:找出長(zhǎng)度大于1分鐘的曲目publicSet<String>findLongTracks(List<Album>albums){returnalbums.stream().flatMap(album->album.getTracks()).filter(track->track.getLength()>60).map(track->track.getName()).collect(toSet());}簡(jiǎn)而言之,選取一段遺留代碼進(jìn)行重構(gòu),轉(zhuǎn)換成使用流風(fēng)格的代碼。最初只是簡(jiǎn)單地使用流,但沒有引入任何有用的流操作。隨后通過一系列重構(gòu),最終使代碼更符合使用流的風(fēng)格。在上述步驟中我們沒有提到一個(gè)重點(diǎn),即編寫示例代碼的每一步都要進(jìn)行單元測(cè)試,保證代碼能夠正常工作。重構(gòu)遺留代碼時(shí),這樣做很有幫助。3.5多次調(diào)用流操作用戶也可以選擇每一步強(qiáng)制對(duì)函數(shù)求值,而不是將所有的方法調(diào)用鏈接在一起,但是,最好不要如此操作。例3-24展示了如何用如上述不建議的編碼風(fēng)格來(lái)找出專輯上所有演出樂隊(duì)的國(guó)籍,例3-25則是之前的代碼,放在一起方便比較。例3-24誤用Stream的例子List<Artist>musicians=album.getMusicians().collect(toList());List<Artist>bands=musicians.stream().filter(artist->artist.getName().startsWith("The")).collect(toList());Set<String>origins=bands.stream().map(artist->artist.getNationality()).collect(toSet());例3-2
溫馨提示
- 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 自動(dòng)化施工方案
- 幼兒園大班《滑梯的回憶》教案
- 建筑施工特種作業(yè)-高處作業(yè)吊籃安裝拆卸工真題庫(kù)-2
- 容錯(cuò)性定義題目及答案
- 1 1 集合-2026版53高考數(shù)學(xué)總復(fù)習(xí)A版精煉
- 2023-2024學(xué)年云南省保山市高二下學(xué)期期末質(zhì)量檢測(cè)數(shù)學(xué)試題(解析版)
- 2023-2024學(xué)年山東省青島市萊西市高二下學(xué)期期末考試數(shù)學(xué)試題(解析版)
- 新疆盛鼎龍新材料科技有限責(zé)任公司2500噸-年高效偶聯(lián)劑5000噸-年甲基苯基硅油及3萬(wàn)噸-年硅酮膠項(xiàng)目環(huán)評(píng)報(bào)告
- 2025年秋三年級(jí)上冊(cè)語(yǔ)文同步教案 8 總也倒不了的老屋
- 物流公司和客戶合作協(xié)議
- 醫(yī)療機(jī)構(gòu)審核管理制度
- 華南理工綜評(píng)機(jī)測(cè)試題(一)
- 浙江省杭州市臨平區(qū)2023-2024學(xué)年五年級(jí)下學(xué)期期末語(yǔ)文試卷
- 智能倉(cāng)庫(kù)與倉(cāng)儲(chǔ)管理自動(dòng)化
- 2024-2025部編人教版2二年級(jí)語(yǔ)文下冊(cè)全冊(cè)測(cè)試卷【共10套附答案】
- 第一課能源史簡(jiǎn)介
- 醫(yī)療器械倉(cāng)庫(kù)管理課件
- 2024年火電電力職業(yè)技能鑒定考試-600MW超臨界機(jī)組運(yùn)行筆試參考題庫(kù)含答案
- 2024年全國(guó)工會(huì)財(cái)務(wù)知識(shí)大賽備賽試題庫(kù)500(含答案)
- 24春國(guó)家開放大學(xué)《地域文化(本)》形考任務(wù)1-4參考答案
- 茯苓規(guī)范化生產(chǎn)技術(shù)規(guī)程
評(píng)論
0/150
提交評(píng)論