圖解Stream之collect:長(zhǎng)文深度分析讓你徹底掌握流式編程
在 Java 8 中,引入了 Stream 流的概念,它是對(duì)集合數(shù)據(jù)進(jìn)行操作的一種高級(jí)抽象。Stream 具有以下幾個(gè)主要特點(diǎn)和優(yōu)勢(shì):
- 聲明式編程 通過簡(jiǎn)潔的方式表達(dá)對(duì)數(shù)據(jù)的處理邏輯,而無需關(guān)注具體的實(shí)現(xiàn)細(xì)節(jié)。例如,使用 filter 方法篩選出符合條件的元素,使用 map 方法對(duì)元素進(jìn)行轉(zhuǎn)換。
- 懶加載Stream 的操作并非立即執(zhí)行,而是在終端操作(如 collect、forEach 等)被調(diào)用時(shí)才真正執(zhí)行。這有助于提高性能,避免不必要的計(jì)算。
- 鏈?zhǔn)讲僮?可以將多個(gè)操作連接在一起,形成一個(gè)連貫的處理流程,使代碼更具可讀性和可維護(hù)性。
- 并行處理 可以方便地實(shí)現(xiàn)并行計(jì)算,充分利用多核 CPU 的優(yōu)勢(shì),提高處理大規(guī)模數(shù)據(jù)的效率。
而在 Stream 流中,collect 操作是一個(gè)終端操作,用于將 Stream 中的元素收集到一個(gè)新的集合或數(shù)據(jù)結(jié)構(gòu)中。Stream 提供了對(duì)數(shù)據(jù)的一系列中間操作,如 filter、map、sorted 等,這些操作只是定義了對(duì)數(shù)據(jù)的處理邏輯,但不會(huì)真正執(zhí)行對(duì)數(shù)據(jù)的處理。而 collect 操作作為終端操作,觸發(fā)之前定義的中間操作的執(zhí)行,并將處理后的結(jié)果進(jìn)行收集。
總之,collect 操作是 Stream 流處理中的關(guān)鍵一步,用于將處理后的元素以指定的方式進(jìn)行收集和匯總。下面我們對(duì)collect相關(guān)的操作原理及方法進(jìn)行詳細(xì)地介紹,確保我們完全掌握collect的使用。
Collectors介紹
我們先看看Collect、Collector和Collectors的區(qū)別:
- collect 是 Java 8 中 Stream 流的一個(gè)方法,用于對(duì)流中的元素進(jìn)行收集操作。它需要傳入一個(gè)實(shí)現(xiàn)了 Collector 接口的收集器來指定具體的收集行為。
- Collector 是一個(gè)接口,定義了收集流元素的規(guī)范和方法。通過實(shí)現(xiàn) Collector 接口,可以自定義收集器來實(shí)現(xiàn)特定的元素收集邏輯。
- Collectors 是一個(gè)工具類,它提供了許多靜態(tài)方法,用于方便地創(chuàng)建常見的 Collector 實(shí)現(xiàn)。這些預(yù)定義的收集器可以滿足大多數(shù)常見的收集需求,例如將流元素收集到列表、集合、映射等,或者進(jìn)行分組、分區(qū)、規(guī)約匯總等操作。
例如,使用 Collectors.toList() 可以創(chuàng)建一個(gè)將流元素收集到列表的收集器,然后將其傳遞給 collect 方法,對(duì)流進(jìn)行收集操作并得到一個(gè)包含所有元素的列表。
圖片
概括來說:
- collect 是 Stream 流的終止方法,使用傳入的收集器(必須是 Collector 接口的某個(gè)具體實(shí)現(xiàn)類)對(duì)結(jié)果執(zhí)行相關(guān)操作。
- Collector 是一個(gè)接口,collect 方法接收的收集器是 Collector 接口的具體實(shí)現(xiàn)類。
- Collectors 是一個(gè)工具類,提供了很多靜態(tài)工廠方法,用于創(chuàng)建各種預(yù)定義的 Collector 接口的具體實(shí)現(xiàn)類,方便程序員使用。如果不使用 Collectors 類,自己去實(shí)現(xiàn) Collector 接口也是可以的。
圖片
Collectors的方法
圖片
恒等處理
指的就是Stream的元素在經(jīng)過Collector函數(shù)處理前后完全不變,例如toList()操作,只是最終將結(jié)果從Stream中取出放入到List對(duì)象中,并沒有對(duì)元素本身做任何的更改處理。
圖片
歸約匯總
Stream流中的元素被逐個(gè)遍歷,進(jìn)入到Collector處理函數(shù)中,然后會(huì)與上一個(gè)元素的處理結(jié)果進(jìn)行合并處理,并得到一個(gè)新的結(jié)果,以此類推,直到遍歷完成后,輸出最終的結(jié)果。
圖片
分組分區(qū)
Collectors工具類中提供了groupingBy和partitioningBy方法進(jìn)行數(shù)據(jù)分區(qū),區(qū)別在于partitioningBy僅基于條件分成兩個(gè)組。
圖片
Collector的原理
要自定義收集器Collector,需要實(shí)現(xiàn)Collector接口中定義的五個(gè)方法,分別是:supplier()、accumulator()、combiner()、finisher()和characteristics()。
圖片
這5個(gè)方法的含義說明歸納如下:
接口名稱 | 功能含義說明 |
supplier | 創(chuàng)建新的結(jié)果容器,可以是一個(gè)容器,也可以是一個(gè)累加器實(shí)例,總之是用來存儲(chǔ)結(jié)果數(shù)據(jù)的 |
accumlator | 元素進(jìn)入收集器中的具體處理操作 |
finisher | 當(dāng)所有元素都處理完成后,在返回結(jié)果前的對(duì)結(jié)果的最終處理操作,當(dāng)然也可以選擇不做任何處理,直接返回 |
combiner | 各個(gè)子流的處理結(jié)果最終如何合并到一起去,比如并行流處理場(chǎng)景,元素會(huì)被切分為好多個(gè)分片進(jìn)行并行處理,最終各個(gè)分片的數(shù)據(jù)需要合并為一個(gè)整體結(jié)果,即通過此方法來指定子結(jié)果的合并邏輯 |
characteristics | 對(duì)此收集器處理行為的補(bǔ)充描述,比如此收集器是否允許并行流中處理,是否finisher方法必須要有等等,此處返回一個(gè)Set集合,里面的候選值是固定的幾個(gè)可選項(xiàng)。 |
對(duì)于characteristics返回set集合中的可選值,說明如下:
取值 | 含義說明 |
UNORDERED | 無序。聲明此收集器的匯總歸約結(jié)果與Stream流元素遍歷順序無關(guān),不受元素處理順序影響 |
CONCURRENT | 并行。聲明此收集器可以多個(gè)線程并行處理,允許并行流中進(jìn)行處理 |
IDENTITY_FINISH | 恒等映射。聲明此收集器的finisher方法是一個(gè)恒等操作 |
現(xiàn)在,我們知道了這5個(gè)接口方法各自的含義與用途了,那么作為一個(gè)Collector收集器,這幾個(gè)接口之間是如何配合處理并將Stream數(shù)據(jù)收集為需要的輸出結(jié)果的呢?下面這張圖可以清晰的闡述這一過程:
圖片
如果我們的Collector是支持在并行流中使用的,則其處理過程有所不同:
圖片
下面的例子展示如何自定義一個(gè)將元素收集到LinkedList的收集器:
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
public class MyCollector implements Collector<String, List<String>, List<String>> {
// supplier()方法返回一個(gè)Supplier,它創(chuàng)建了一個(gè)空的LinkedList實(shí)例,作為收集數(shù)據(jù)的容器。
@Override
public Supplier<List<String>> supplier() {
return LinkedList::new;
}
// accumulator()方法返回一個(gè)BiConsumer,用于將流中的元素添加到LinkedList中。
@Override
public BiConsumer<List<String>, String> accumulator() {
return List::add;
}
// combiner()方法返回一個(gè)BinaryOperator,用于合并多個(gè)LinkedList。當(dāng)流被并行處理時(shí),可能會(huì)有多個(gè)子部分的結(jié)果需要合并,這里將兩個(gè)LinkedList合并為一個(gè)。
@Override
public BinaryOperator<List<String>> combiner() {
return (r1, r2) -> {
r1.addAll(r2);
return r1;
};
}
// finisher()方法返回一個(gè)Function,在遍歷完流后,將累加器對(duì)象(在這里就是LinkedList本身)轉(zhuǎn)換為最終結(jié)果。在這個(gè)例子中,累加器對(duì)象就是最終結(jié)果,所以直接返回它。
@Override
public Function<List<String>, List<String>> finisher() {
return list -> list;
}
// characteristics()方法返回一個(gè)包含收集器特征的EnumSet。這里使用了IDENTITY_FINISH特征,表示finisher方法返回的是一個(gè)恒等函數(shù),可以跳過,直接將累加器作為最終結(jié)果。
@Override
public EnumSet<Collector.Characteristics> characteristics() {
return EnumSet.of(Collector.Characteristics.IDENTITY_FINISH);
}
}
下面我們用自定義的收集器進(jìn)行處理:
List<String> input = Arrays.asList("apple", "banana", "orange");
List<String> result = input.stream().collect(new MyCollector());
如果希望收集器具有其他特性,例如支持并行處理(CONCURRENT)、不保證元素順序(UNORDERED)等,可以在characteristics()方法中添加相應(yīng)的特性。例如,如果你的收集器支持并行處理且不保證元素順序,可以這樣返回特性集合:
return EnumSet.of(Collector.Characteristics.CONCURRENT, Collector.Characteristics.UNORDERED);
另外,還可以根據(jù)具體的需求自定義收集器的邏輯,例如過濾元素、執(zhí)行特定的計(jì)算等。
Collectors方法深究
groupingBy分組
Collectors.groupingBy是 Java 8 中Stream API 的一個(gè)收集器,用于將流中的元素根據(jù)某個(gè)分類函數(shù)收集到Map中。
groupingBy的構(gòu)造方法
- groupingBy(Function):基本的分組,默認(rèn)使用List收集,
圖片
相當(dāng)于groupingBy(classifier, toList())。我們用下面的代碼實(shí)現(xiàn),對(duì)學(xué)生按照年齡段進(jìn)行分組:
Map<Integer, List<Student>> nameListByAge = students.stream().collect(Collectors.groupingBy(Student::getAge));
- groupingBy(Function, Collector):可指定收集器的分組
圖片
這里使用Set集合收集。
// 不同年齡段的學(xué)生集合,去重
Map<Integer, Set<String>> namesByAge = students.stream().collect(Collectors.groupingBy(
Student::getAge,
Collectors.mapping(Student::getName, Collectors.toSet()))
);
- groupingBy(Function, Supplier, Collector):可指定存儲(chǔ)容器和收集器的分組
圖片
下面使用TreeMap作為容器,保證了鍵的有序性。但是分組之后的組內(nèi)數(shù)據(jù)不是有序的。
// 【鍵有序】不同年齡段的學(xué)生集合,去重,年齡按照升序排列
Map<Integer, Set<String>> namesBySortedAge = students.stream().collect(Collectors.groupingBy(
Student::getAge,
TreeMap::new,
Collectors.mapping(Student::getName, Collectors.toSet()))
);
如果要保證分組之后的數(shù)據(jù)有序,有下面兩種方法:
- collectingAndThen:先分組,再使用collectingAndThen聚合操作,對(duì)組內(nèi)數(shù)據(jù)進(jìn)行排序。
Map<Integer, List<Student>> sortedCollect = students.stream()
.collect(Collectors.groupingBy(
Student::getAge,
Collectors.collectingAndThen(
// 先收集到List
Collectors.toList(),
// 然后對(duì)每個(gè)List進(jìn)行排序
list -> list.stream().sorted(Comparator.comparing(Student::getScore)).collect(Collectors.toList())
)
));
- mapping:使用第二種構(gòu)造方法,對(duì)組內(nèi)元素收集到list,然后使用TreeSet集合進(jìn)行收集。
// 按照年齡分組,組內(nèi)按照分?jǐn)?shù)升序
Map<Integer, TreeSet<Student>> collect = students.stream().collect(Collectors.groupingBy(
Student::getAge,
Collectors.mapping(student -> student, Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Student::getScore))))
)
);
基礎(chǔ)分組功能
- 按照對(duì)象的某個(gè)字段進(jìn)行分組:假設(shè)有一個(gè)學(xué)生類Student,包含course(課程)字段,可以按照課程對(duì)學(xué)生進(jìn)行分組。
Map<String, List<Student>> groupByCourse = students.stream()
.collect(Collectors.groupingBy(Student::getCourse));
- 自定義鍵的映射:根據(jù)學(xué)生對(duì)象的多個(gè)字段或進(jìn)行某種格式化操作來生成鍵。
Map<String, List<Student>> groupByCustomKey = students.stream()
.collect(Collectors.groupingBy(student -> student.getName() + "_" + student.getAge()));
- 自定義容器類型:如使用LinkedHashMap保證分組后鍵的有序性。
Map<String, List<Student>> groupByCourseWithLinkedHashMap = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, LinkedHashMap::new, Collectors.toList()));
分組統(tǒng)計(jì)功能
- 計(jì)數(shù):計(jì)算每個(gè)分組中的元素?cái)?shù)量。
Map<String, Long> courseCountMap = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.counting()));
- 求和:對(duì)每個(gè)分組中的某個(gè)數(shù)值字段進(jìn)行求和。
Map<String, Integer> totalScoreByCourseMap = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.summingInt(Student::getScore)));
- 平均值:計(jì)算每個(gè)分組中某個(gè)數(shù)值字段的平均值。
Map<String, Double> averageScoreByCourseMap = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.averagingInt(Student::getScore)));
- 最大最小值:獲取每個(gè)分組中某個(gè)數(shù)值字段的最大值或最小值。
Map<String, Student> maxScoreStudentByCourseMap = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.maxBy(Comparator.comparingInt(Student::getScore))));
Map<String, Student> minScoreStudentByCourseMap = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.minBy(Comparator.comparingInt(Student::getScore))));
- 完整統(tǒng)計(jì):同時(shí)獲取計(jì)數(shù)、總和、平均值、最大最小值等統(tǒng)計(jì)結(jié)果。
Map<String, IntSummaryStatistics> summaryStatisticsByCourseMap = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.summarizingInt(Student::getScore)));
- 范圍統(tǒng)計(jì):根據(jù)某個(gè)條件進(jìn)行范圍分組統(tǒng)計(jì)。
Map<Boolean, List<Student>> dividedByScore = students.stream()
.collect(Collectors.partitioningBy(student -> student.getScore() >= 60));
分組合并功能
合并分組結(jié)果:使用reducing方法對(duì)每個(gè)分組的元素進(jìn)行自定義的合并操作。
Map<String, String> combinedNamesByCourseMap = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.reducing("", Student::getName, (name1, name2) -> name1 + ", " + name2)));
合并字符串:將每個(gè)分組中的字符串元素連接起來。
Map<String, String> joinedNamesByCourseMap = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.joining(", ")));
分組自定義映射功能
映射結(jié)果為Collection對(duì)象:將每個(gè)分組的元素映射為另一個(gè)Collection對(duì)象。
Map<String, Set<Student>> studentsSetByCourseMap = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.toSet()));
自定義映射結(jié)果:通過mapping方法進(jìn)行更復(fù)雜的映射操作。
Map<String, List<String>> studentNamesByCourseMap = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.mapping(Student::getName, Collectors.toList())));
自定義downstream收集器:更靈活地控制分組后的值的收集方式。
Collector<Student,?, Map<String, CustomResult>> customCollector = Collector.of(
HashMap::new,
(map, student) -> {
// 自定義的收集邏輯,將學(xué)生對(duì)象轉(zhuǎn)換為 CustomResult 并添加到 map 中
},
(map1, map2) -> {
// 合并兩個(gè) map 的邏輯
});
Map<String, CustomResult> customResultMap = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, customCollector));
多級(jí)分組可以通過嵌套使用groupingBy來實(shí)現(xiàn)。例如,假設(shè)有一個(gè)包含學(xué)生信息的列表,要先按班級(jí)分組,然后在每個(gè)班級(jí)內(nèi)再按性別分組,可以這樣寫:
Map<String, Map<String, List<Student>>> groupedByClassAndGender = students.stream()
.collect(Collectors.groupingBy(Student::getClass, Collectors.groupingBy(Student::getGender)));
在上述示例中,外層的groupingBy按照班級(jí)進(jìn)行分組,得到的每個(gè)班級(jí)的分組結(jié)果(本身也是一個(gè)Map)又通過內(nèi)層的groupingBy按照性別進(jìn)一步分組。這樣最終得到的是一個(gè)兩級(jí)分組的Map結(jié)構(gòu)。
partitioningBy分類
掌握了groupingBy,現(xiàn)在看partitioningBy就簡(jiǎn)單很多了。就兩個(gè)簡(jiǎn)單的構(gòu)造方法:
// 僅提供分類器
partitioningBy(Predicate<? super T> predicate)
// 提供分類器和下游收集器
partitioningBy(Predicate<? super T> predicate,Collector<? super T, A, D> downstream)
比如我們篩選成年人和非成年人:
Map<Boolean, List<Student>> adultList = students.stream().collect(Collectors.partitioningBy(s -> s.getAge() > 18));
Map<Boolean, Set<Student>> adultSet = students.stream().collect(Collectors.partitioningBy(s -> s.getAge() > 18, Collectors.toSet()));
結(jié)果如下圖所示:
圖片
collectingAndThen分組處理
圖片
從方法簽名可以看出,需要傳入一個(gè)收集器和一個(gè)處理函數(shù),相當(dāng)于收集了數(shù)據(jù)之后,再進(jìn)行后續(xù)操作。如下圖所示:
圖片
比如,前面提到的,先分組,再排序:
Map<Integer, List<Student>> sortedCollect = students.stream()
.collect(Collectors.groupingBy(
Student::getAge,
Collectors.collectingAndThen(
// 先收集到List
Collectors.toList(),
// 然后對(duì)每個(gè)List進(jìn)行排序
list -> list.stream().sorted(Comparator.comparing(Student::getScore)).collect(Collectors.toList())
)
));
reducing歸集操作
單參數(shù):輸入歸集操作
- BinaryOperator accumulator 歸集操作函數(shù) 輸入?yún)?shù)T返回T
圖片
比如實(shí)現(xiàn)數(shù)組的內(nèi)容求和:
List<Integer> testData = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Optional<Integer> sum = testData.stream().collect(Collectors.reducing((prev, cur) -> {
System.out.println("prev=>" + prev + "cur=>" + cur);
return prev + cur;
}));
System.out.print(sum.get()); // 45
雙參數(shù):輸入初始值、歸集操作 參數(shù)說明
- T identity 返回類型T初始值
- BinaryOperator accumulator 歸集操作函數(shù) 輸入?yún)?shù)T返回T
下面是增加了初始值的求和操作:
List<Integer> testData = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Integer sum = testData.stream().collect(Collectors.reducing(20, (prev, cur) -> {
System.out.println("prev=>" + prev + "cur=>" + cur);
return prev + cur;
}));
System.out.print(sum); //65
三參數(shù):這個(gè)函數(shù)才是真正體現(xiàn)reducing(歸集)的過程。調(diào)用者要明確知道以下三點(diǎn)
- 需要轉(zhuǎn)換類型的初始值
- 類型如何轉(zhuǎn)換
- 如何收集返回值
參數(shù)說明
- U identity 最終返回類型U初始值
- BiFunction<U, ? super T, U> accumulator, 將輸入?yún)?shù)T轉(zhuǎn)換成返回類型U的函數(shù)
- BinaryOperator combiner 歸集操作函數(shù) 輸入?yún)?shù)U返回U
圖片
比如實(shí)現(xiàn)單數(shù)字轉(zhuǎn)字符串并按逗號(hào)連接的功能:
List<Integer> testData = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
String joinStr = testData.stream().collect(Collectors.reducing("轉(zhuǎn)換成字符串", in -> {
return in + "";
}, (perv, cur) -> {
return perv + "," + cur;
}));
System.out.print(joinStr); // 轉(zhuǎn)換成字符串,1,2,3,4,5,6,7,8,9