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