Lambda 表达式详解~Streams API~规约操作
上一節(jié)介紹了部分Stream常見接口方法,理解起來并不困難,但Stream的用法不止于此,本節(jié)我們將仍然以Stream為例,介紹流的規(guī)約操作。
規(guī)約操作(reduction operation)又被稱作折疊操作(fold),是通過某個(gè)連接動(dòng)作將所有元素匯總成一個(gè)匯總結(jié)果的過程。元素求和、求最大值或最小值、求出元素總個(gè)數(shù)、將所有元素轉(zhuǎn)換成一個(gè)列表或集合,都屬于規(guī)約操作。Stream類庫有兩個(gè)通用的規(guī)約操作reduce()和collect(),也有一些為簡(jiǎn)化書寫而設(shè)計(jì)的專用規(guī)約操作,比如sum()、max()、min()、count()等。
最大或最小值這類規(guī)約操作很好理解(至少方法語義上是這樣),我們著重介紹reduce()和collect(),這是比較有魔法的地方。
多面手reduce()
reduce操作可以實(shí)現(xiàn)從一組元素中生成一個(gè)值,sum()、max()、min()、count()等都是reduce操作,將他們單獨(dú)設(shè)為函數(shù)只是因?yàn)槌S?。reduce()的方法定義有三種重寫形式:
-
Optional<T> reduce(BinaryOperator<T> accumulator)
-
T reduce(T identity, BinaryOperator<T> accumulator)
-
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
雖然函數(shù)定義越來越長(zhǎng),但語義不曾改變,多的參數(shù)只是為了指明初始值(參數(shù)identity),或者是指定并行執(zhí)行時(shí)多個(gè)部分結(jié)果的合并方式(參數(shù)combiner)。reduce()最常用的場(chǎng)景就是從一堆值中生成一個(gè)值。用這么復(fù)雜的函數(shù)去求一個(gè)最大或最小值,你是不是覺得設(shè)計(jì)者有病。其實(shí)不然,因?yàn)椤按蟆焙汀靶 被蛘摺扒蠛?#34;有時(shí)會(huì)有不同的語義。
需求:從一組單詞中找出最長(zhǎng)的單詞。這里“大”的含義就是“長(zhǎng)”。
// 找出最長(zhǎng)的單詞 Stream<String> stream = Stream.of("I", "love", "you", "too"); Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2); //Optional<String> longest = stream.max((s1, s2) -> s1.length()-s2.length()); System.out.println(longest.get());上述代碼會(huì)選出最長(zhǎng)的單詞love,其中Optional是(一個(gè))值的容器,使用它可以避免null值的麻煩。當(dāng)然可以使用Stream.max(Comparator<? super T> comparator)方法來達(dá)到同等效果,但reduce()自有其存在的理由。
?需求:求出一組單詞的長(zhǎng)度之和。這是個(gè)“求和”操作,操作對(duì)象輸入類型是String,而結(jié)果類型是Integer。
// 求單詞長(zhǎng)度之和 Stream<String> stream = Stream.of("I", "love", "you", "too"); Integer lengthSum = stream.reduce(0, // 初始值 // (1)(sum, str) -> sum+str.length(), // 累加器 // (2)(a, b) -> a+b); // 部分和拼接器,并行執(zhí)行時(shí)才會(huì)用到 // (3) // int lengthSum = stream.mapToInt(str -> str.length()).sum(); System.out.println(lengthSum);上述代碼標(biāo)號(hào)(2)處將i. 字符串映射成長(zhǎng)度,ii. 并和當(dāng)前累加和相加。這顯然是兩步操作,使用reduce()函數(shù)將這兩步合二為一,更有助于提升性能。如果想要使用map()和sum()組合來達(dá)到上述目的,也是可以的。
reduce()擅長(zhǎng)的是生成一個(gè)值,如果想要從Stream生成一個(gè)集合或者M(jìn)ap等復(fù)雜的對(duì)象該怎么辦呢?終極武器collect()橫空出世!
終極武器collect()
不夸張的講,如果你發(fā)現(xiàn)某個(gè)功能在Stream接口中沒找到,十有八九可以通過collect()方法實(shí)現(xiàn)。collect()是Stream接口方法中最靈活的一個(gè),學(xué)會(huì)它才算真正入門Java函數(shù)式編程。先看幾個(gè)熱身的小例子:
// 將Stream轉(zhuǎn)換成容器或Map Stream<String> stream = Stream.of("I", "love", "you", "too"); List<String> list = stream.collect(Collectors.toList()); // (1) // Set<String> set = stream.collect(Collectors.toSet()); // (2) // Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); // (3)上述代碼分別列舉了如何將Stream轉(zhuǎn)換成List、Set和Map。雖然代碼語義很明確,可是我們?nèi)匀粫?huì)有幾個(gè)疑問:
Function.identity()是干什么的?
String::length是什么意思?
Collectors是個(gè)什么東西?
接口的靜態(tài)方法和默認(rèn)方法
Function是一個(gè)接口,那么Function.identity()是什么意思呢?這要從兩方面解釋:
Java 8允許在接口中加入具體方法。接口中的具體方法有兩種,default方法和static方法,identity()就是Function接口的一個(gè)靜態(tài)方法。
Function.identity()返回一個(gè)輸出跟輸入一樣的Lambda表達(dá)式對(duì)象,等價(jià)于形如t -> t形式的Lambda表達(dá)式。
上面的解釋是不是讓你疑問更多?不要問我為什么接口中可以有具體方法,也不要告訴我你覺得t -> t比identity()方法更直觀。我會(huì)告訴你接口中的default方法是一個(gè)無奈之舉,在Java 7及之前要想在定義好的接口中加入新的抽象方法是很困難甚至不可能的,因?yàn)樗袑?shí)現(xiàn)了該接口的類都要重新實(shí)現(xiàn)。試想在Collection接口中加入一個(gè)stream()抽象方法會(huì)怎樣?default方法就是用來解決這個(gè)尷尬問題的,直接在接口中實(shí)現(xiàn)新加入的方法。既然已經(jīng)引入了default方法,為何不再加入static方法來避免專門的工具類呢!
方法引用
諸如String::length的語法形式叫做方法引用(method references),這種語法用來替代某些特定形式Lambda表達(dá)式。如果Lambda表達(dá)式的全部?jī)?nèi)容就是調(diào)用一個(gè)已有的方法,那么可以用方法引用來替代Lambda表達(dá)式。方法引用可以細(xì)分為四類:
| 引用靜態(tài)方法 | Integer::sum |
| 引用某個(gè)對(duì)象的方法 | list::add |
| 引用某個(gè)類的方法 | String::length |
| 引用構(gòu)造方法 | HashMap::new |
我們會(huì)在后面的例子中使用方法引用。
收集器
相信前面繁瑣的內(nèi)容已徹底打消了你學(xué)習(xí)Java函數(shù)式編程的熱情,不過很遺憾,下面的內(nèi)容更繁瑣。但這不能怪Stream類庫,因?yàn)橐獙?shí)現(xiàn)的功能本身很復(fù)雜。
?
收集器(Collector)是為Stream.collect()方法量身打造的工具接口(類)??紤]一下將一個(gè)Stream轉(zhuǎn)換成一個(gè)容器(或者M(jìn)ap)需要做哪些工作?我們至少需要兩樣?xùn)|西:
目標(biāo)容器是什么?是ArrayList還是HashSet,或者是個(gè)TreeMap。
新元素如何添加到容器中?是List.add()還是Map.put()。
如果并行的進(jìn)行規(guī)約,還需要告訴collect()?3. 多個(gè)部分結(jié)果如何合并成一個(gè)。
結(jié)合以上分析,collect()方法定義為<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner),三個(gè)參數(shù)依次對(duì)應(yīng)上述三條分析。不過每次調(diào)用collect()都要傳入這三個(gè)參數(shù)太麻煩,收集器Collector就是對(duì)這三個(gè)參數(shù)的簡(jiǎn)單封裝,所以collect()的另一定義為<R,A> R collect(Collector<? super T,A,R> collector)。Collectors工具類可通過靜態(tài)方法生成各種常用的Collector。舉例來說,如果要將Stream規(guī)約成List可以通過如下兩種方式實(shí)現(xiàn):
// 將Stream規(guī)約成List Stream<String> stream = Stream.of("I", "love", "you", "too"); List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1 //List<String> list = stream.collect(Collectors.toList());// 方式2 System.out.println(list);通常情況下我們不需要手動(dòng)指定collect()的三個(gè)參數(shù),而是調(diào)用collect(Collector<? super T,A,R> collector)方法,并且參數(shù)中的Collector對(duì)象大都是直接通過Collectors工具類獲得。實(shí)際上傳入的收集器的行為決定了collect()的行為。
使用collect()生成Collection
前面已經(jīng)提到通過collect()方法將Stream轉(zhuǎn)換成容器的方法,這里再匯總一下。將Stream轉(zhuǎn)換成List或Set是比較常見的操作,所以Collectors工具已經(jīng)為我們提供了對(duì)應(yīng)的收集器,通過如下代碼即可完成:
// 將Stream轉(zhuǎn)換成List或Set Stream<String> stream = Stream.of("I", "love", "you", "too"); List<String> list = stream.collect(Collectors.toList()); // (1) Set<String> set = stream.collect(Collectors.toSet()); // (2)上述代碼能夠滿足大部分需求,但由于返回結(jié)果是接口類型,我們并不知道類庫實(shí)際選擇的容器類型是什么,有時(shí)候我們可能會(huì)想要人為指定容器的實(shí)際類型,這個(gè)需求可通過Collectors.toCollection(Supplier<C> collectionFactory)方法完成。
// 使用toCollection()指定規(guī)約容器的類型 ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3) HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)上述代碼(3)處指定規(guī)約結(jié)果是ArrayList,而(4)處指定規(guī)約結(jié)果為HashSet。一切如你所愿。
使用collect()生成Map
前面已經(jīng)說過Stream背后依賴于某種數(shù)據(jù)源,數(shù)據(jù)源可以是數(shù)組、容器等,但不能是Map。反過來從Stream生成Map是可以的,但我們要想清楚Map的key和value分別代表什么,根本原因是我們要想清楚要干什么。通常在三種情況下collect()的結(jié)果會(huì)是Map:
使用Collectors.toMap()生成的收集器,用戶需要指定如何生成Map的key和value。
使用Collectors.partitioningBy()生成的收集器,對(duì)元素進(jìn)行二分區(qū)操作時(shí)用到。
使用Collectors.groupingBy()生成的收集器,對(duì)元素做group操作時(shí)用到。
情況1:使用toMap()生成的收集器,這種情況是最直接的,前面例子中已提到,這是和Collectors.toCollection()并列的方法。如下代碼展示將學(xué)生列表轉(zhuǎn)換成由<學(xué)生,GPA>組成的Map。非常直觀,無需多言。
// 使用toMap()統(tǒng)計(jì)學(xué)生GPA Map<Student, Double> studentToGPA =students.stream().collect(Collectors.toMap(Function.identity(),// 如何生成keystudent -> computeGPA(student)));// 如何生成value情況2:使用partitioningBy()生成的收集器,這種情況適用于將Stream中的元素依據(jù)某個(gè)二值邏輯(滿足條件,或不滿足)分成互補(bǔ)相交的兩部分,比如男女性別、成績(jī)及格與否等。下列代碼展示將學(xué)生分成成績(jī)及格或不及格的兩部分。
// Partition students into passing and failing Map<Boolean, List<Student>> passingFailing = students.stream().collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));情況3:使用groupingBy()生成的收集器,這是比較靈活的一種情況。跟SQL中的group by語句類似,這里的groupingBy()也是按照某個(gè)屬性對(duì)數(shù)據(jù)進(jìn)行分組,屬性相同的元素會(huì)被對(duì)應(yīng)到Map的同一個(gè)key上。下列代碼展示將員工按照部門進(jìn)行分組:
// Group employees by department Map<Department, List<Employee>> byDept = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment));以上只是分組的最基本用法,有些時(shí)候僅僅分組是不夠的。在SQL中使用group by是為了協(xié)助其他查詢,比如1. 先將員工按照部門分組,2. 然后統(tǒng)計(jì)每個(gè)部門員工的人數(shù)。Java類庫設(shè)計(jì)者也考慮到了這種情況,增強(qiáng)版的groupingBy()能夠滿足這種需求。增強(qiáng)版的groupingBy()允許我們對(duì)元素分組之后再執(zhí)行某種運(yùn)算,比如求和、計(jì)數(shù)、平均值、類型轉(zhuǎn)換等。這種先將元素分組的收集器叫做上游收集器,之后執(zhí)行其他運(yùn)算的收集器叫做下游收集器(downstream Collector)。
// 使用下游收集器統(tǒng)計(jì)每個(gè)部門的人數(shù) Map<Department, Integer> totalByDept = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment,Collectors.counting()));// 下游收集器上面代碼的邏輯是不是越看越像SQL?高度非結(jié)構(gòu)化。還有更狠的,下游收集器還可以包含更下游的收集器,這絕不是為了炫技而增加的把戲,而是實(shí)際場(chǎng)景需要??紤]將員工按照部門分組的場(chǎng)景,如果我們想得到每個(gè)員工的名字(字符串),而不是一個(gè)個(gè)Employee對(duì)象,可通過如下方式做到:
// 按照部門對(duì)員工分布組,并只保留員工的名字 Map<Department, List<String>> byDept = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment,Collectors.mapping(Employee::getName,// 下游收集器Collectors.toList())));// 更下游的收集器如果看到這里你還沒有對(duì)Java函數(shù)式編程失去信心,恭喜你,你已經(jīng)順利成為Java函數(shù)式編程大師了。
使用collect()做字符串join
這個(gè)肯定是大家喜聞樂見的功能,字符串拼接時(shí)使用Collectors.joining()生成的收集器,從此告別for循環(huán)。Collectors.joining()方法有三種重寫形式,分別對(duì)應(yīng)三種不同的拼接方式。無需多言,代碼過目難忘。
// 使用Collectors.joining()拼接字符串 Stream<String> stream = Stream.of("I", "love", "you"); //String joined = stream.collect(Collectors.joining());// "Iloveyou" //String joined = stream.collect(Collectors.joining(","));// "I,love,you" String joined = stream.collect(Collectors.joining(",", "{", "}"));// "{I,love,you}"collect()還可以做更多
除了可以使用Collectors工具類已經(jīng)封裝好的收集器,我們還可以自定義收集器,或者直接調(diào)用collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)方法,收集任何形式你想要的信息。不過Collectors工具類應(yīng)該能滿足我們的絕大部分需求,手動(dòng)實(shí)現(xiàn)之間請(qǐng)先看看文檔。
總結(jié)
以上是生活随笔為你收集整理的Lambda 表达式详解~Streams API~规约操作的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 小甲鱼 OllyDbg 教程系列 (十二
- 下一篇: 汇编语言 test 和 cmp 区别