




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