lambda就是數學中的“λ”的讀音,lambda表達式是基于λ演算而得名的,因為lambda抽象(lambda abstraction)表示一個匿名的函數,于是開發語言也將lambda表達式用來表示匿名函數,也就是沒有函數名字的函數。C#、Python,甚至是C++都有lambda表達式語法。為了提高開發者的開發效率,并照顧“跨語言”開發者的開發習慣,Java語言也加入了lambda表達式。流處理是Java程序中一種重要的數據處理手段,它用少量的代碼便可以執行復雜的數據過濾、映射、查找和收集等功能。
本章知識架構及重難點如下:14.1 lambda表達式
14.1.1 lambda表達式簡介
lambda表達式可以用非常少的代碼實現抽象方法。lambda表達式不能獨立執行,因此必須實現函數式接口,并且會返回一個函數式接口的對象。lambda表達式的語法非常特殊,語法格式如下:
() -> 結果表達式
參數 -> 結果表達式
(參數1,參數2....,參數n)-> 結果表達式
1行實現無參方法,方法體是操作符右側代碼塊。
2行實現只有一個參數的方法,方法體是操作符右側代碼塊。
3行實現多參數的方法,方法體是操作符右側代碼塊。
lambda表達式的語法非常抽象,并且有著非常強大的自動化功能,如自動識別泛型、自動數據類型轉換等,這會讓初學者很難掌握。如果將lambda表達式的功能歸納總結,可以將lambda表達式語法用如下方式理解:
() (代碼塊》
這個方法 按照 這樣的代碼來實現
?簡單總結:操作符左側的是方法參數,操作符右側的是方法體。
誤區警示
“->”符號是由英文狀態下的“-”和“>”組成的,符號之間沒有空格。
14.1.2 lambda表達式實現函數式接口
lambda表達式可以實現函數式接口,本節將講解函數式接口概念以及用lambda表達式實現不同類型的函數式接口。
1.函數式接口
函數式接口指的是僅包含一個抽象方法的接口,接口中的方法簡單明了地說明了接口的用途,如線程接口Runnable、動作事件監聽接口ActionListener等。開發者可以創建自定義的函數式接口,例如:
interface MyInterface (
void method();
】
如果接口中包含一個以上的抽象方法,則不符合函數式接口的規范,這樣的接口不能用lambda表達式創建匿名對象。本章內容中所有被lambda表達式實現的接口均為函數式接口。
2.lambda表達式實現無參抽象方法
很多函數式接口的抽放方法是無參數的,如線程接口Runnable接口只有一個run()方法,這樣的無參抽象方法在lambda表達式中使用“( )”表示。
【例14.1】使用lambda表達式實現打招呼接口
創建函數式接口和測試類,接口抽象方法為無參方法并返回一個字符串。使用lambda表達式實現接口,讓方法可以輸出當前日期。
?本實例直接在lambda表達式中創建SayHiInterface接口對象,并指定了一個字符串作為接口方法的返回值。最后在輸出語句中,pi對象就是lambda表達式創建出的對象,當pi調用接口方法時就輸出了lambda表達式指定的字符串。
3.lambda表達式實現有參抽象方法
抽象方法中有一個或多個參數的函數式接口也是很常見的,lambda表達式中可以用“(a1,a2,a3)”的方法表示有參抽象方法,圓括號里標識符對應抽象方法的參數。如果抽象方法中只有一個參數,lambda表達式則可以省略圓括號。
【例14.2】使用lambda表達式做加法計算
創建函數式接口和測試類,接口抽象方法有兩個參數并返回一個int型結果。使用lambda表達式實現接口,讓方法可以計算兩個整數的和,具體代碼如下:
?在這個實例中,函數式接口的抽象方法有兩個參數,lambda表達式的圓括號內也寫了兩個參數對應的抽象方法。這里有一個點要注意,lambda表達式中的參數不需要與抽象方法的參數名稱相同,但順序必須相同。
4.lambda表達式使用代碼塊
當函數式接口的抽象方法需要實現復雜邏輯而不是返回一個簡單的表達式的話,就需要在lambda表達式中使用代碼塊。lambda表達式會自動判斷返回值類型是否符合抽象方法的定義。
【例14.3】使用lambda表達式為考試成績分類
創建函數式接口和測試類,接口抽象方法有一個整型參數表示成績,輸入成績后,返回成績的字符串評語。在lambda表達式中實現成績判斷。
?14.1.3 lambda表達式調用外部變量
lambda表達式除了可以調用定義好的參數,還可以調用表達式以外的變量。但是,這些外部的變量有些可以被更改,有些則不能。例如,lambda表達式無法更改局部變量的值,但是卻可以更改外部類的成員變量(也可以叫作類屬性)的值。
1.lambda表達式無法更改局部變量
局部變量在lambda表達式中默認被定義為final(靜態)的,也就是說,lambda表達式只能調用局部變量,卻不能改變其值。
【例14.4】使用lambda表達式修改局部變量
創建函數式接口和測試類,在測試類的main()方法中創建局部變量和接口對象,接口對象使用lambda表達式實現,并在lambda表達式中嘗試更改局部變量值。
?2.lambda表達式可以更改類成員變量
類成員變量是在lambda表達式中不是被final修飾的,所以lambda表達式可以改變其值。
【例14.5】使用lambda表達式修改類成員變量
創建函數式接口和測試類,在測試類中創建成員屬性value和成員方法action()。在action()方法中使用lambda表達式創建接口對象,并在lambda表達式中修改value的值。運行程序,查看value值是否發生變化。
?從這個結果可以看出以下幾點:
ambda表達式可以調用并修改類成員變量的值。
ambda表達式只是描述了抽象方法是如何實現的,在抽象方法沒有被調用前,lambda表達式中的代碼并沒有被執行,所以運行抽象方法之前類成員變量的值不會發生變化。
要抽象方法被調用,就會執行lambda表達式中的代碼,類成員變量的值就會被修改。
14.1.4 lambda表達式與異常處理
很多接口的抽象方法為了保證程序的安全性,會在定義時就拋出異常。但是lambda表達式中并沒有拋出異常的語法,這是因為lambda表達式會默認拋出抽象方法原有的異常,當此方法被調用時則需要進行異常處理。
【例14.6】使用lambda表達式實現防沉迷接口
創建自定義異常UnderAgeException,當發現用戶是未成年人時進入此異常處理。創建函數式接口,在抽象方法中拋出UnderAgeException異常,使用lambda表達式實現此接口,并讓接口對象執行抽象方法。
?從這個實例中可以看出,即使lambda表達式沒有定義異常,原抽象方法拋出的異常仍然是存在的,當接口對象執行此方法時會被強制要求進行異常處理。
14.2.1 引用靜態方法
引用靜態方法的語法如下:
類名::靜態方法名
這個語法中出現了一個新的操作符“::”,這是由兩個英文冒號組成的操作符,冒號之間沒有空格。這個操作符左邊表示方法所屬的類名,右邊是方法名。需要注意的是,這個語法中方法名是沒有圓括號的。
【例14.7】使用lambda表達式引用靜態方法
創建函數式接口和測試類,在接口中定義抽象方法method(),在測試類中編寫一個可以用來實現抽象方法的靜態方法——add()方法。在main()方法中創建接口對象,并使用引用靜態方法的語法讓接口對象的抽象方法按照測試類的add()方法來實現。
?從這個結果可以看出,接口方法得出的結果正是按照add()方法中的邏輯計算出來的。
14.2.2 引用成員方法
引用成員方法的語法如下:
對象名::成員方法名
與引用靜態方法語法不同,這里操作符左側的必須是一個對象名,而不是類名。這種語法也可以達到抽象方法按照類成員方法邏輯來實現的目的。
【例14.8】使用lambda表達式引用成員方法
創建函數式接口和測試類,在接口中定義抽象方法method(),在測試類中編寫一個可以用來實現抽象方法的成員方法——format()方法。在main()方法中創建接口對象,并使用引用成員方法的語法讓接口對象的抽象方法按照測試類的format()方法來實現。
?從這個結果可以看出,抽象方法的結果是按照類成員方法的邏輯計算出來的。
14.2.3 引用帶泛型的方法
泛型是Java開發經常使用到的功能,“::”操作符支持引用帶泛型的方法。除方法外,“::”操作符也支持引用帶泛型的類。
【例14.9】使用lambda表達式引用帶泛型的方法
創建函數式接口和測試類,在接口定義時添加泛型T,并且在抽象方法參數中使用此泛型。在測試類中創建帶有泛型的靜態方法,同樣在方法參數中使用此泛型。抽象方法和類靜態方法參數類型保持一致。類靜態方法會利用哈希集合不保存重復數據的原理,實現過濾數組中的重復數據。
?注意
與其他使用泛型的場景一樣,要保證代碼前后泛型一致,否則會發生編譯錯誤。
14.2.4 引用構造方法
lambda表達式有3種引用構造方法的語法,分別是引用無參構造方法、引用有參構造方法和引用數組構造方法,下面分別進行講解。
1.引用無參構造方法
引用構造方法的語法如下:
類名::new
因為構造方法與類名相同,如果操作符左右都寫類名,會讓操作符誤以為是在引用與類名相同的靜態方法,這樣會導致程序出現Bug,所以引用構造方法的語法使用了new關鍵字。操作符右側的寫new關鍵字,表示引用構造方法。
這個語法有一點要注意:new關鍵字之后沒有圓括號,也沒有參數的定義。如果類中既有無參構造方法,又有有參構造方法,使用引用構造方法語法后,究竟哪一個構造方法被引用了呢?引用哪個構造方法是由函數式接口決定的,“::”操作符會返回與抽象方法的參數結構相同的構造方法。如果找不到參數接口相同的構造方法,則會發生編譯錯誤。
【例14.10】使用lambda表達式引用無參構造方法
創建函數式接口和測試類。測試類中創建一個無參構造方法和一個有參構造方法。接口抽象方法返回值為測試類對象,并且方法無參數。使用引用構造方法語法創建接口對象,調用接口對象方法創建測試類對象,查看輸出結果。
?從這個結果可以看出,如果接口方法沒有參數,調用的就是無參的構造方法。
2.引用有參構造方法
引用有參構造方法的語法與引用無參構造方法一樣。區別就是函數式接口的抽象方法是有參數的。
【例14.11】使用lambda表達式引用有參數的構造方法
創建函數式接口和測試類。測試類創建一個無參構造方法和一個有參構造方法。接口抽象方法返回值為測試類對象,并且方法參數結構要和測試類有參構造方法參數一致。使用引用構造方法語法創建接口對象,調用接口對象方法創建測試類對象,查看輸出結果。
?從這個結果可以看出,無參構造方法沒有被調用,接口方法使用的就是有參數的構造方法。
3.引用數組構造方法
Java開發可能出現這樣一種特殊場景:把數組類型當作泛型。如果方法返回值是泛型,在這種特殊場景下,方法就應該返回一個數組類型的結果。如果要求抽象方法既引用構造方法,又要返回數組類型結果,這種場景下抽象方法的參數就有了另外一個含義:數組個數。抽象方法的參數可以決定返回的數組長度,但數組中的元素并不是有值的,還需要再次賦值。引用數組構造方法的語法也會有所不同,語法如下:
類名[]::new
【例14.12】使用lambda表達式引用數組的構造方法
創建函數式接口和測試類。定義接口時創建一個泛型T,同時T作為抽象方法的返回值。抽象方法定義一個整型參數。創建接口對象時,將測試類數組作為泛型,并引用數組構造方法。通過接口方法創建測試類數組,再分別為每個數組元素賦值。
實例中不能給array[3]賦值,因為接口方法的參數是3,創建的數組只包含3個元素。
14.2.5 Fuction接口
在此之前的所有實例中,想要使用lambda表達式都需要先創建或調用已有的函數式接口,但java.util.function包已經提供了很多預定義函數式接口,就是沒有實現任何功能,僅用來封裝lambda表達式的對象。該包中最常用的接口是Function<T,R>接口,這個接口有以下兩個泛型:
:被操作的類型,可以理解為方法參數類型。
:操作結果類型,可以理解為方法的返回類型。
Function接口是函數式接口,所以只有一個抽象方法,但是Function接口還提供了3個已實現的方法以方便開發者對函數邏輯進行更深層的處理。Function接口方法如表14.1所示。
表14.1 Function接口方法
【例14.13】使用lambda表達式拼接IP地址
創建Function接口對象,使用lambda表達式實現拼接IP地址的功能,具體代碼如下:
?流處理有點類似數據庫的SQL語句,可以執行非常復雜的過濾、映射、查找和收集功能,并且代碼量很少。唯一的缺點是代碼可讀性不高,如果開發者基礎不好,可能會看不懂流API所表達的含義。
為了能讓讀者更好地理解流API的處理過程和結果,本節先創建一個公共類——Employee員工類。員工類包含員工的姓名、年齡、薪資、性別和部門屬性,并給這些屬性提供了getter方法。重寫toString()可以方便查看員工對象的所有信息。公共類提供了一個靜態方法getEmpList(),這個方法已經創建好了一些員工對象,然后將這些員工封裝成一個集合并返回。本節將重點對這些員工數據進行流處理。
員工集合的詳細數據如表14.2所示。
表14.2 公共類提供已定義好的員工集合數據
?【例14.14】創建員工類,并按照表14.2創建初始化數據
創建Employee類,在類中創建姓名、年齡、工資、性別和部門屬性,創建對應這些屬性的構造方法和Getter方法。最后將初始化的員工數據放到一個ArrayList列表中。
? 14.3.1 Stream接口簡介
流處理的接口都定義在java.uil.stream包下。BaseStream接口是最基礎的接口,但最常用的是BaseStream接口的一個子接口——Stream接口,基本上絕大多數的流處理都是在Stream接口上實現的。
Stream接口是泛型接口,所以流中操作的元素可以是任何類的對象。Stream接口的常用方法如表14.3所示。
表14.3 Stream接口的常用方法
? 說明
表14.3中最后一列“類型”中有兩個值:中間操作和終端操作。中間操作類型的方法會生成一個新的流對象,被操作的流對象仍然可以執行其他操作;終端操作會消費流,操作結束之后,被操作的流對象就不能再次執行其他操作了。這是兩者的最大區別。
Collection接口新增兩個可以獲取流對象的方法。第一個方法最常用,可以獲取集合的順序流,方法如下:
Stream<E> stream();
第二個方法可以獲取集合的并行流,方法如下:
Stream<E> parallelstream();
因為所有集合類都是Collection接口的子類,如ArrayList類、HashSet類等,所以這些類都可以進行流處理。例如:
List<Integer> list =new ArrayList<Integer>(); //創建集合
Stream<Integer> s = list.stream(); //獲取集合流對象
14.3.2 Optional類
Optional類像是一個容器,可以保存任何對象,并且針對NullPointerException空指針異常做了優化,保證Optional類保存的值不會是null。因此,Optional類是針對“對象可能是null也可能不是null”的場景為開發者提供了優質的解決方案,減少了煩瑣的異常處理。
Optional類是用final修飾的,所以不能有子類。Optional類是帶有泛型的類,所以該類可以保存任何對象的值。
從Optional類的聲明代碼中就可以看出這些特性,JDK中的部分代碼如下:
public final class Optional<T>(
private final T value;
//省略其他代碼
? Optional類中有一個叫作value的成員屬性,這個屬性就是用來保存具體值的。value是用泛型T修飾的,并且還用了final修飾,這表示一個Optional對象只能保存一個值。
Optional類提供了很多封裝、校驗和獲取值的方法,這些方法如表14.4所示。
表14.4 Optional類提供的常用方法
說明
除Optional類外,還可以使用OptionalDouble類、OptionalInt類和OptionalLong類這3個類,開發者可以根據不同的應用場景靈活選擇。
【例14.15】使用Optional類創建“空”對象
創建一個Optional對象,并賦予一個字符串類型的值,然后判斷此對象的值是否為空;再使用empty()方法創建一個“空值”的Optional對象,然后判斷此對象的值是否為空。
?14.3.3 Collectors類
Collectors類為收集器類,該類實現了java.util.Collector接口,可以將Stream流對象進行各種各樣的封裝、歸集、分組等操作。同時,Collectors類還提供了很多實用的數據加工方法,如數據統計計算等。Collectors類的常用方法如表14.5所示。
表14.5 Collectors類的常用方法
? Collectors類的具體用法將在后面做重點講解。
14.3.4 數據過濾
數據過濾就是在雜亂的數據中篩選出需要的數據,類似SQL語句中的WHERE關鍵字,給出一定的條件,將符合條件的數據過濾并展示出來。
1.filter()方法
filter()方法是Stream接口提供的過濾方法。該方法可以將lambda表達式作為參數,然后按照lambda表達式的邏輯過濾流中的元素。過濾出想要的流元素后,還需使用Stream提供的collect()方法按照指定方法重新封裝。
【例14.16】輸出1~10中的所有奇數
將1~10的數字放到一個ArrayList列表中,調用該列表的Stream對象的filter()方法,方法參數為過濾奇數的lambda表達式。查看方法執行完畢后Stream對象返回的結果。
?這個實例把“獲取流”“過濾流”“封裝流”3個部分操作分開編寫,是為了方便讀者學習理解,通常為了代碼簡潔,3部分操作可以寫在一行代碼中,例如:
List<Integer> result = list.stream().filter(n -> n % 2 == 1).collect(Collectors.toList());
這種寫法也可以避免終端操作造成的“流被消費掉”的問題,因為每次被操作的流都是從集合中重新獲取的。
說明
代碼在Eclipse中出現黃色警告,這是printeach(String message, List list)方法中list參數沒有指定泛型引起的。但是,這也體現出lambda表達式可以自動識別數據類型的優點。讀者可以忽略此警告。
例14.16中演示的集合元素是數字類型,集合能保存的不止是數字,下面這個實例將演示如何利用過濾器以對象屬性為條件過濾元素。
【例14.17】找出年齡大于30的員工
本實例使用了例14.14定義的Employee類,在獲取員工集合后,將年齡大于30的員工過濾出來。如果將員工集合返回的流對象泛型定義為<Employee>,就可以直接在lambda表達式中使用Employee類的方法。
?通過這個結果可以看出,年齡沒超過30的員工都被過濾掉了。通過類的一個屬性,就可以將符合條件的類對象完整地獲取到,員工的姓名、性別等屬性都可以打印出來。
2.distinct()方法
distinct()方法是Stream接口提供的過濾方法。該方法可以去除流中的重復元素,效果與SQL語句中的DISTINCT關鍵字一樣。
【例14.18】去除List集合中的重復數字
創建一個List集合,保存一些數字(包含重復數字),獲取集合的流對象,使用distinct()方法將重復的數字去掉。
?因為distinct()方法屬于中間操作,所以可以配合filter()方法一起使用。
3.limit()方法
limit()方法是Stream接口提供的方法,該方法可以獲取流中前N個元素。
【例14.19】找出所有員工列表中的前兩位女員工
本實例使用了例14.14定義的Employee類,在獲取員工集合后,取出所有員工中的前兩位女員工,具體代碼如下:
?4.skip()方法
skip()方法是Stream接口提供的方法,該方法可以忽略流中的前N個元素。
【例14.20】取出所有男員工,并忽略前兩位男員工
本實例使用了例14.14定義的Employee類,在獲取員工集合后,取出所有男員工,并忽略前兩位男員工。
?14.3.5 數據映射
數據的映射和過濾概念不同:過濾是在流中找到符合條件的元素,映射是在流中獲得具體的數據。
Stream接口提供了map()方法用來實現數據映射,map()方法會按照參數中的函數邏輯獲取新的流對象,新的流對象中元素類型可能與舊流對象元素類型不相同。
【例14.21】獲取開發部所有員工的名單
本實例使用了例14.14定義的Employee類,在獲取員工集合后,先過濾出開發部的員工,再引用員工類的getName()方法作為map()方法的映射參數,這樣就獲取到開發部員工名單。
?結果輸出了開發部兩位員工的名字,但沒有輸出這兩位員工的其他信息,這個就是映射的結果。
除了可以映射出員工名單,還可以對映射數據進行加工處理。例如,統計銷售部一個月的薪資總額。因為涉及數字計算,所以需要讓Stream對象轉為可以進行數學運算的數字流。因為薪資類型是double類型,所以應該調用mapToDouble()方法進行轉換。
【例14.22】計算銷售部一個月的薪資總額
本實例使用了例14.14定義的Employee類,在獲取員工集合后,先過濾出銷售部的員工,再引用員工類的getSalary()方法作為mapToDouble()的映射參數,獲取到DoubleStream對象后,調用該對象的sum()方法,就可以計算出銷售部的薪資總和。具體代碼如下:
除DoubleStream類外,java.util.stream包還提供了IntStream類和LongStream類以應對不同的計算場景。
14.3.6 數據查找
本節所講的數據查找并不是在流中獲取數據(這屬于數據過濾),而是判斷流中是否有符合條件的數據,查找的結果是一個boolean值或一個Optional類的對象。本節將講解allMatch()、anyMatch()、noneMatch()和findFirst()這4個方法。
?1.allMatch()方法
allMatch()方法是Stream接口提供的方法,該方法會判斷流中的元素是否全部符合某一條件,返回結果是boolean值。如果所有元素都符合條件則返回true,否則返回false。
【例14.23】檢查所有員工是否都大于25歲(實例位置:資源包\TM\sl\14\23)
本例使用了例14.14定義的Employee類,在獲取員工集合后,使用allMatch()方法檢查公司所有員工的年齡是否都大于25歲,具體代碼如下:
?最后得出的結果是false,因為公司里的“小馬”和“小王”的年齡都只有21歲,而“小劉”的年齡只有24歲,不滿足“所有員工都大于25”這個條件。
2.anyMatch()方法
anyMatch()方法是Stream接口提供的方法,該方法會判斷流中的元素是否有符合某一條件,只要有一個元素符合條件就返回true,如果沒有元素符合條件才會返回false。
【例14.24】檢查是否有年齡大于40歲的員工
本例使用了例14.14定義的Employee類,在獲取員工集合后,使用anyMatch()方法檢查公司里是否有年齡在40歲或40歲以上的員工,具體代碼如下:
?運行結果為true,因為公司里的“老張”正好40歲,符合“有年齡在40歲或以上的員工”的條件。
3.noneMatch()方法
noneMatch()方法是Stream接口提供的方法,該方法會判斷流中的所有元素是否都不符合某一條件。這個方法的邏輯和allMatch()方法正好相反。
【例14.25】檢查公司是否不存在薪資小于2000元的員工
本例使用了例14.14定義的Employee類,在獲取員工集合后,使用noneMatch()方法檢查公司里是否沒有薪資小于2000的員工,具體代碼如下:
?公司最低薪資是3000,也就是說沒有員工的薪資會小于2000,所以結果為true。
4.findFirst()方法
findFirst()方法是Stream接口提供的方法,這個方法會返回符合條件的第一個元素。
【例14.26】找出第一個年齡等于21歲的員工(實例位置:資源包\TM\sl\14\26)
本例使用了例14.14定義的Employee類,在獲取員工集合后,首先將年齡為21歲的員工過濾出來,然后使用findFirst()方法獲取第一個員工,具體代碼如下:
?公司里有兩個21歲的員工,一個是“小馬”,一個是“小王”。因為“小馬”在集合中的位置靠前,所以findFirst()方法獲取的是“小馬”。
注意
這個方法的返回值不是boolean值,而是一個Optional對象。
14.3.7 數據收集
數據收集可以理解為高級的“數據過濾+數據映射”,是對數據的深加工。本節將講解兩種實用場景:數據統計和數據分組。
1.數據統計
數據統計不僅可以篩選出特殊元素,還可以對元素的屬性進行統計計算。這種復雜的統計操作不是由Stream實現的,而是由Collectors收集器類實現的,收集器提供了非常豐富的API,有著強大的數據挖掘能力。
【例14.27】統計公司各項數據,打印成報表
本例使用了例14.14定義的Employee類,在獲取員工集合后,不使用filter()方法對元素進行過濾,而直接使用Stream接口和Collectors類的方法對公司各項數據進行統計,具體代碼如下:
?這是一個復雜的例子,里面涉及人數統計、比較年齡取最大年齡和最小年齡員工、統計薪資總和、求平均薪資等。最后兩項比較特殊,一個是獲取數字統計類DoubleSummaryStatistics類,這個類本身就包含了個數、總和、均值、最大值、最小值這5個屬性;另一個在獲取員工映射名單時又加工了一下,將所有名字拼接成了一個字符串,并在名字之間用逗號分隔。
2.數據分組
數據分組就是將流中元素按照指定的條件分開保存,類似SQL語言中的“GROUP BY”關鍵字。分組之后的數據會按照不同的標簽分別保存成一個集合,然后按照“鍵-值”關系封裝在Map對象中。
數據分組有一級分組和多級分組兩種場景,首先先來介紹一級分組。
一級分組,就是將所有數據按照一個條件進行歸類。例如,學校有100個學生,這些學生分布在3個年級中。學生按照年級分成了3組,然后就不再細分了,這就屬于一級分組。
Collectors類提供的groupingBy()方法就是用來進行分組的方法,方法參數是一個Function接口對象,收集器會按照指定的函數規則對數據進行分組。
【例14.28】將所有員工按照部門分組
本例使用了例14.14定義的Employee類,在獲取員工集合后,創建Function接口對象f,f引用Employee員工類的getDept()方法獲取部門名稱。然后,流的收集器類按照f的規則進行分組,Stream對象將分組結果賦值給一個Map對象,Map對象將會以“key:部門,value:員工List”的方式保存數據。具體代碼如下:
組規則是一個函數,這個函數是由Collectors收集器類調用的,而不是Stream流對象。
ap<K,List<T>>有兩個泛型,第一個泛型是組的類型,第二個是組內的元素集合類型。實例中按照部門名稱分組,所以K的類型是String類型;部門內的元素是員工集合,所以List集合泛型T的類型就應該是Employee類型。
介紹完一級分組后,再介紹一下復雜的多級分組。
?一級分組是按照一個條件進行分組,那么多級分組就是按照多個條件進行分組。還是用學校舉例,學校有100個學生,這些學生分布在3個年級中,這是一級分組,但每個年級還有若干個班級,學生們分到不同年級之后又分到不同的班,這就是二級分組。如果學生再按男女分組,就變成了三級分組。元素按照兩個以上的條件進行分組,就是多級分組。
Collectors類提供的groupingBy()方法還提供了一個重載形式:
groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
【例14.29】將所有員工先按照部門分組,再按照性別分組
在一級分組實例的基礎上,首先創建Function接口對象deptFunc用于引用獲取部門的方法,再創建Function接口對象sexFunc用于引用獲取性別的方法(這兩個對象將作為一級分組和二級分組的函數規則),最后將按照性別分組的Collectors.groupingBy(sexFunc)方法作為另一個groupingBy()方法的參數,按照部門進行分組,這樣就實現了二級分組,具體代碼如下:
?
? 這個結果先按照部門進行了分組,然后又對部門中的男女進行了二級分組。這個實例也有兩個難點:
例中兩個groupingBy()方法的參數不一樣,一個是groupingBy(性別分組規則),另一個是groupingBy(部門分組規則, groupingBy(性別分組規則) )。
得的Map對象中,還嵌套了Map對象,它的結構是這樣的:
Map<部門, Map<性別, List<員工>>>
從左數,第一個Map對象做了一級分組,第二個Map對象做了二級分組。
?
?