一、Java 8 Stream簡介
Java 8 API添加了一個新的抽象稱為流Stream,可以讓你以一種聲明的方式處理數據。Stream 使用一種類似用 SQL 語句從數據庫查詢數據的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。
1.1 什么是 Stream?
Stream(流)是一個來自數據源的元素隊列并支持聚合操作
- 元素 是特定類型的對象,形成一個隊列。 Java中的Stream并不會存儲元素,而是按需計算。
- 數據源 流的來源,可以是集合,數組,I/O channel, 產生器generator 等。
- 聚合操作 類似SQL語句一樣的操作, 比如filter, map, reduce, find, match, sorted等。
和以前的Collection操作不同, Stream操作還有兩個基礎的特征:
- Pipelining: 中間操作都會返回流對象本身。 這樣多個操作可以串聯成一個管道, 如同流式風格(fluent style)。 這樣做可以對操作進行優化, 比如延遲執行(laziness)和短路( short-circuiting)。
- 內部迭代: 以前對集合遍歷都是通過Iterator或者For-Each的方式, 顯式的在集合外部進行迭代, 這叫做外部迭代。 Stream提供了內部迭代的方式, 通過訪問者模式(Visitor)實現。
1.2 流式處理
我們希望對一個包含整數的集合中篩選出所有的偶數,并將其封裝成為一個新的List返回,那么在Java8之前,我們需要通過如下代碼實現
List<Integer> evens = new ArrayList<>();
for (final Integer num : nums) {
if (num % 2 == 0) {
evens.add(num);
}
}
通過java8的流式處理,我們可以將代碼簡化為
List<Integer> evens = nums.stream().filter(num -> num % 2 == 0).collect(Collectors.toList());
先簡單解釋一下上面這行語句的含義,stream()操作將集合轉換成一個流,filter()執行我們自定義的篩選處理,這里是通過lambda表達式篩選出所有偶數,最后我們通過collect()對結果進行封裝處理,并通過Collectors.toList()指定其封裝成為一個List集合返回。
由上面的例子可以看出,Java8的流式處理極大的簡化了對于集合的操作,實際上不光是集合,包括數組、文件等,只要是可以轉換成流,我們都可以借助流式處理,類似于我們寫SQL語句一樣對其進行操作。Java8通過內部迭代來實現對流的處理,一個流式處理可以分為三個部分:轉換成流、中間操作、終端操作。
以集合為例,一個流式處理的操作我們首先需要調用stream()函數將其轉換成流,然后再調用相應的中間操作達到我們需要對集合進行的操作,比如篩選、轉換等,最后通過終端操作對前面的結果進行封裝,返回我們需要的形式。
1.3 Java7與Java8實現代碼對比
1.3.1 Java7代碼
import java.util.*;
/**
* @author Alan Chen
* @description Java7
* @date 2021/11/22
*/
public class Java7Tester {
public static void main(String args[]) {
System.out.println("使用 Java 7: ");
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
System.out.println("列表: " + strings);
// 計算空字符串
long count = getCountEmptyStringUsingJava7(strings);
System.out.println("空字符數量為: " + count);
// 字符串長度為3的數量為
count = getCountLength3UsingJava7(strings);
System.out.println("字符串長度為3的數量為: " + count);
// 刪除空字符串
List<String> filtered = deleteEmptyStringsUsingJava7(strings);
System.out.println("刪除空字符串后的列表: " + filtered);
// 刪除空字符串,并使用逗號把它們合并起來
String mergedString = getMergedStringUsingJava7(strings, ", ");
System.out.println("合并字符串: " + mergedString);
// 獲取列表元素平方數
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
List<Integer> squaresList = getSquares(numbers);
System.out.println("平方數列表: " + squaresList);
//列表數據統計
List<Integer> integers = Arrays.asList(1, 2, 13, 4, 15, 6, 17, 8, 19);
System.out.println("列表: " + integers);
System.out.println("列表中最大的數 : " + getMax(integers));
System.out.println("列表中最小的數 : " + getMin(integers));
System.out.println("所有數之和 : " + getSum(integers));
System.out.println("平均數 : " + getAverage(integers));
System.out.println("隨機數: ");
// 輸出10個隨機數
Random random = new Random();
for (int i = 0; i < 10; i++) {
System.out.println(random.nextInt());
}
}
private static int getCountEmptyStringUsingJava7(List<String> strings) {
int count = 0;
for (String string : strings) {
if (string.isEmpty()) {
count++;
}
}
return count;
}
private static int getCountLength3UsingJava7(List<String> strings) {
int count = 0;
for (String string : strings) {
if (string.length() == 3) {
count++;
}
}
return count;
}
private static List<String> deleteEmptyStringsUsingJava7(List<String> strings) {
List<String> filteredList = new ArrayList<String>();
for (String string : strings) {
if (!string.isEmpty()) {
filteredList.add(string);
}
}
return filteredList;
}
private static String getMergedStringUsingJava7(List<String> strings, String separator) {
StringBuilder stringBuilder = new StringBuilder();
for (String string : strings) {
if (!string.isEmpty()) {
stringBuilder.append(string);
stringBuilder.append(separator);
}
}
String mergedString = stringBuilder.toString();
return mergedString.substring(0, mergedString.length() - 2);
}
private static List<Integer> getSquares(List<Integer> numbers) {
List<Integer> squaresList = new ArrayList<Integer>();
for (Integer number : numbers) {
Integer square = new Integer(number.intValue() * number.intValue());
if (!squaresList.contains(square)) {
squaresList.add(square);
}
}
return squaresList;
}
private static int getMax(List<Integer> numbers) {
int max = numbers.get(0);
for (int i = 1; i < numbers.size(); i++) {
Integer number = numbers.get(i);
if (number.intValue() > max) {
max = number.intValue();
}
}
return max;
}
private static int getMin(List<Integer> numbers) {
int min = numbers.get(0);
for (int i = 1; i < numbers.size(); i++) {
Integer number = numbers.get(i);
if (number.intValue() < min) {
min = number.intValue();
}
}
return min;
}
private static int getSum(List numbers) {
int sum = (int) (numbers.get(0));
for (int i = 1; i < numbers.size(); i++) {
sum += (int) numbers.get(i);
}
return sum;
}
private static int getAverage(List<Integer> numbers) {
return getSum(numbers) / numbers.size();
}
}
結果
"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" "-javaagent:D:\install\IntelliJ IDEA 2020.1\lib\idea_rt.jar=57428:D:\install\IntelliJ IDEA 2020.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar;E:\code\test\out\production\test" Java7Tester
使用 Java 7:
列表: [abc, , bc, efg, abcd, , jkl]
空字符數量為: 2
字符串長度為3的數量為: 3
刪除空字符串后的列表: [abc, bc, efg, abcd, jkl]
合并字符串: abc, bc, efg, abcd, jkl
平方數列表: [9, 4, 49, 25]
列表: [1, 2, 13, 4, 15, 6, 17, 8, 19]
列表中最大的數 : 19
列表中最小的數 : 1
所有數之和 : 85
平均數 : 9
隨機數:
-1621840016
1848281340
-956050294
1802920185
-1521363590
1796922091
2027850453
1804093167
-840853541
-1529616196
Process finished with exit code 0
1.3.2 Java8代碼
import java.util.*;
import java.util.stream.Collectors;
/**
* @author Alan Chen
* @description Java8
* @date 2021/11/22
*/
public class Java8Tester {
public static void main(String args[]) {
System.out.println("使用 Java 8: ");
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
System.out.println("列表: " + strings);
// 空字符串數量為
long count = strings.stream().filter(string -> string.isEmpty()).count();
System.out.println("空字符串數量為: " + count);
// 字符串長度為3的數量為
count = strings.stream().filter(string -> string.length() == 3).count();
System.out.println("字符串長度為3的數量為: " + count);
// 刪除空字符串后的列表
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
System.out.println("刪除空字符串后的列表: " + filtered);
// 刪除空字符串,并使用逗號把它們合并起來
String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
System.out.println("合并字符串: " + mergedString);
// 獲取列表元素平方數
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
List<Integer> squaresList = numbers.stream().map(i -> i * i).distinct().collect(Collectors.toList());
System.out.println("Squares List: " + squaresList);
//列表數據統計
List<Integer> integers = Arrays.asList(1, 2, 13, 4, 15, 6, 17, 8, 19);
System.out.println("列表: " + integers);
IntSummaryStatistics stats = integers.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("列表中最大的數 : " + stats.getMax());
System.out.println("列表中最小的數 : " + stats.getMin());
System.out.println("所有數之和 : " + stats.getSum());
System.out.println("平均數 : " + stats.getAverage());
System.out.println("隨機數: ");
// 輸出10個隨機數
Random random = new Random();
random.ints().limit(10).sorted().forEach(System.out::println);
// 并行處理
count = strings.parallelStream().filter(string -> string.isEmpty()).count();
System.out.println("空字符串的數量為: " + count);
}
}
結果
"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" "-javaagent:D:\install\IntelliJ IDEA 2020.1\lib\idea_rt.jar=57436:D:\install\IntelliJ IDEA 2020.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar;E:\code\test\out\production\test" Java8Tester
使用 Java 8:
列表: [abc, , bc, efg, abcd, , jkl]
空字符串數量為: 2
字符串長度為3的數量為: 3
刪除空字符串后的列表: [abc, bc, efg, abcd, jkl]
合并字符串: abc, bc, efg, abcd, jkl
Squares List: [9, 4, 49, 25]
列表: [1, 2, 13, 4, 15, 6, 17, 8, 19]
列表中最大的數 : 19
列表中最小的數 : 1
所有數之和 : 85
平均數 : 9.444444444444445
隨機數:
-788107954
-559847792
61268113
334297559
568203527
637005978
674422513
809980552
1415190011
1892961238
空字符串的數量為: 2
Process finished with exit code 0
二、中間操作
我們定義一個簡單的學生實體類,用于后面的例子演示
public class Student {
/** 學號 */
private long id;
private String name;
private int age;
/** 年級 */
private int grade;
/** 專業 */
private String major;
/** 學校 */
private String school;
// 省略getter和setter
}
// 初始化
List<Student> students = new ArrayList<Student>() {
{
add(new Student(20160001, "孔明", 20, 1, "土木工程", "武漢大學"));
add(new Student(20160002, "伯約", 21, 2, "信息安全", "武漢大學"));
add(new Student(20160003, "玄德", 22, 3, "經濟管理", "武漢大學"));
add(new Student(20160004, "云長", 21, 2, "信息安全", "武漢大學"));
add(new Student(20161001, "翼德", 21, 2, "機械與自動化", "華中科技大學"));
add(new Student(20161002, "元直", 23, 4, "土木工程", "華中科技大學"));
add(new Student(20161003, "奉孝", 23, 4, "計算機科學", "華中科技大學"));
add(new Student(20162001, "仲謀", 22, 3, "土木工程", "浙江大學"));
add(new Student(20162002, "魯肅", 23, 4, "計算機科學", "浙江大學"));
add(new Student(20163001, "丁奉", 24, 5, "土木工程", "南京大學"));
}
};
2.1 過濾
過濾,顧名思義就是按照給定的要求對集合進行篩選滿足條件的元素,java8提供的篩選操作包括:filter、distinct、limit、skip。
2.1.1 filter
在前面的例子中我們已經演示了如何使用filter,其定義為:Stream<T> filter(Predicate<? super T> predicate)
,filter接受一個謂詞Predicate
,我們可以通過這個謂詞定義篩選條件,Predicate
是一個函數式接口,其包含一個test(T t)
方法,該方法返回boolean
。現在我們希望從集合students
中篩選出所有武漢大學的學生,那么我們可以通過filter來實現,并將篩選操作作為參數傳遞給filter
List<Student> whuStudents = students.stream()
.filter(student -> "武漢大學".equals(student.getSchool()))
.collect(Collectors.toList());
2.1.2 去重
2.1.2.1 distinct去重
distinct操作類似于我們在寫SQL語句時,添加的DISTINCT關鍵字,用于去重處理,distinct基于Object.equals(Object)實現,回到最開始的例子,假設我們希望篩選出所有不重復的偶數,那么可以添加distinct操作
List<Integer> evens = nums.stream()
.filter(num -> num % 2 == 0).distinct()
.collect(Collectors.toList());
List集合去重
public static void main(String[] args) {
List<String> memberIds = new ArrayList<>();
memberIds.add("A");
memberIds.add("B");
memberIds.add("A");
List<String> distinctList = memberIds.stream().distinct().collect(Collectors.toList());
//[A, B]
System.out.println(distinctList);
}
2.1.2.2 對象去重
List<DynamicDetailDTO> detailDTOS = new ArrayList<>();
//去重
detailDTOS = detailDTOS.stream().collect(
Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(DynamicDetailDTO::getId))), ArrayList::new));
2.1.3 limit
limit操作也類似于SQL語句中的LIMIT關鍵字,不過相對功能較弱,limit返回包含前n個元素的流,當集合大小小于n時,則返回實際長度,比如下面的例子返回前兩個專業為土木工程專業的學生
List<Student> civilStudents = students.stream()
.filter(student -> "土木工程".equals(student.getMajor())).limit(2)
.collect(Collectors.toList());
2.1.4 sorted
說到limit,不得不提及一下另外一個流操作:sorted。該操作用于對流中元素進行排序,sorted要求待比較的元素必須實現Comparable接口,如果沒有實現也不要緊,我們可以將比較器作為參數傳遞給sorted(Comparator<? super T> comparator),比如我們希望篩選出專業為土木工程的學生,并按年齡從小到大排序,篩選出年齡最小的兩個學生,那么可以實現為
List<Student> sortedCivilStudents = students.stream()
.filter(student -> "土木工程".equals(student.getMajor())).sorted((s1, s2) -> s1.getAge() - s2.getAge())
.limit(2)
.collect(Collectors.toList());
2.1.4.1 多字段排序
List<類> list; 代表某集合
//返回 對象集合以類屬性一升序排序
list.stream().sorted(Comparator.comparing(類::屬性一));
//返回 對象集合以類屬性一降序排序 注意兩種寫法
list.stream().sorted(Comparator.comparing(類::屬性一).reversed());//先以屬性一升序,結果進行屬性一降序
list.stream().sorted(Comparator.comparing(類::屬性一,Comparator.reverseOrder()));//以屬性一降序
//返回 對象集合以類屬性一升序 屬性二升序
list.stream().sorted(Comparator.comparing(類::屬性一).thenComparing(類::屬性二));
//返回 對象集合以類屬性一降序 屬性二升序 注意兩種寫法
list.stream().sorted(Comparator.comparing(類::屬性一).reversed().thenComparing(類::屬性二));//先以屬性一升序,升序結果進行屬性一降序,再進行屬性二升序
list.stream().sorted(Comparator.comparing(類::屬性一,Comparator.reverseOrder()).thenComparing(類::屬性二));//先以屬性一降序,再進行屬性二升序
//返回 對象集合以類屬性一降序 屬性二降序 注意兩種寫法
list.stream().sorted(Comparator.comparing(類::屬性一).reversed().thenComparing(類::屬性二,Comparator.reverseOrder()));//先以屬性一升序,升序結果進行屬性一降序,再進行屬性二降序
list.stream().sorted(Comparator.comparing(類::屬性一,Comparator.reverseOrder()).thenComparing(類::屬性二,Comparator.reverseOrder()));//先以屬性一降序,再進行屬性二降序
//返回 對象集合以類屬性一升序 屬性二降序 注意兩種寫法
list.stream().sorted(Comparator.comparing(類::屬性一).reversed().thenComparing(類::屬性二).reversed());//先以屬性一升序,升序結果進行屬性一降序,再進行屬性二升序,結果進行屬性一降序屬性二降序
list.stream().sorted(Comparator.comparing(類::屬性一).thenComparing(類::屬性二,Comparator.reverseOrder()));//先以屬性一升序,再進行屬性二降序<br><br><br>
2.1.5 skip
skip操作與limit操作相反,如同其字面意思一樣,是跳過前n個元素,比如我們希望找出排序在2之后的土木工程專業的學生,那么可以實現為
List<Student> civilStudents = students.stream()
.filter(student -> "土木工程".equals(student.getMajor()))
.skip(2)
.collect(Collectors.toList());
通過skip,就會跳過前面兩個元素,返回由后面所有元素構造的流,如果n大于滿足條件的集合的長度,則會返回一個空的集合。
2.2 映射
在SQL中,借助SELECT關鍵字后面添加需要的字段名稱,可以僅輸出我們需要的字段數據,而流式處理的映射操作也是實現這一目的,在java8的流式處理中,主要包含兩類映射操作:map和flatMap。
2.2.1 map
舉例說明,假設我們希望篩選出所有專業為計算機科學的學生姓名,那么我們可以在filter篩選的基礎之上,通過map將學生實體映射成為學生姓名字符串,具體實現如下
List<String> names = students.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.map(Student::getName).collect(Collectors.toList());
除了上面這類基礎的map,java8還提供了mapToDouble(ToDoubleFunction<? super T> mapper)
,mapToInt(ToIntFunction<? super T> mapper)
,mapToLong(ToLongFunction<? super T> mapper)
,這些映射分別返回對應類型的流,Java8為這些流設定了一些特殊的操作,比如我們希望計算所有專業為計算機科學學生的年齡之和,那么我們可以實現如下:
int totalAge = students.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.mapToInt(Student::getAge).sum();
通過將Student按照年齡直接映射為IntStream,我們可以直接調用提供的sum()方法來達到目的,此外使用這些數值流的好處還在于可以避免jvm裝箱操作所帶來的性能消耗。
2.2.2 flatMap
flatMap與map的區別在于 flatMap是將一個流中的每個值都轉成一個個流,然后再將這些流扁平化成為一個流 。舉例說明,假設我們有一個字符串數組String[] strs = {"java8", "is", "easy", "to", "use"};
,我們希望輸出構成這一數組的所有非重復字符
List<String> distinctStrs = Arrays.stream(strs)
.map(str -> str.split("")) // 映射成為Stream<String[]>
.flatMap(Arrays::stream) // 扁平化為Stream<String>
.distinct()
.collect(Collectors.toList());
三、終端操作
終端操作是流式處理的最后一步,我們可以在終端操作中實現對流查找、歸約等操作。
3.1 查找
3.1.1 allMatch
allMatch用于檢測是否全部都滿足指定的參數行為,如果全部滿足則返回true,例如我們希望檢測是否所有的學生都已滿18周歲,那么可以實現為
boolean isAdult = students.stream().allMatch(student -> student.getAge() >= 18);
3.1.2 anyMatch
anyMatch則是檢測是否存在一個或多個滿足指定的參數行為,如果滿足則返回true,例如我們希望檢測是否有來自武漢大學的學生,那么可以實現為
boolean hasWhu = students.stream().anyMatch(student -> "武漢大學".equals(student.getSchool()));
3.1.3 noneMathch
noneMatch用于檢測是否不存在滿足指定行為的元素,如果不存在則返回true,例如我們希望檢測是否不存在專業為計算機科學的學生,可以實現如下
boolean noneCs = students.stream().noneMatch(student -> "計算機科學".equals(student.getMajor()));
3.1.4 findFirst
findFirst用于返回滿足條件的第一個元素,比如我們希望選出專業為土木工程的排在第一個學生,那么可以實現如下
Optional<Student> optStu = students.stream().filter(student -> "土木工程".equals(student.getMajor())).findFirst();
findFirst不攜帶參數,具體的查找條件可以通過filter設置,findFirst返回的是一個Optional類型。
3.1.5 findAny
findAny相對于findFirst的區別在于,findAny不一定返回第一個,而是返回任意一個,比如我們希望返回任意一個專業為土木工程的學生,可以實現如下
Optional<Student> optStu = students.stream().filter(student -> "土木工程".equals(student.getMajor())).findAny();
實際上對于順序流式處理而言,findFirst和findAny返回的結果是一樣的,至于為什么會這樣設計,是因為當我們啟用并行流式處理的時候,查找第一個元素往往會有很多限制,如果不是特別需求,在并行流式處理中使用findAny的性能要比findFirst好。
3.2 歸約
前面的例子中我們大部分都是通過collect(Collectors.toList())
對數據封裝返回,如我的目標不是返回一個新的集合,而是希望對經過參數化操作后的集合進行進一步的運算,那么我們可用對集合實施歸約操作。Java8的流式處理提供了reduce
方法來達到這一目的。
前面我們通過mapToInt
將Stream<Student>
映射成為IntStream
,并通過IntStream
的sum
方法求得所有學生的年齡之和,實際上我們通過歸約操作,也可以達到這一目的,實現如下
// 前面例子中的方法
int totalAge = students.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.mapToInt(Student::getAge).sum();
// 歸約操作
int totalAge = students.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.map(Student::getAge)
.reduce(0, (a, b) -> a + b);
// 進一步簡化
int totalAge2 = students.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.map(Student::getAge)
.reduce(0, Integer::sum);
// 采用無初始值的重載版本,需要注意返回Optional
Optional<Integer> totalAge = students.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.map(Student::getAge)
.reduce(Integer::sum); // 去掉初始值
3.3 收集
前面利用collect(Collectors.toList())
是一個簡單的收集操作,是對處理結果的封裝,對應的還有toSet
、toMap
,以滿足我們對于結果組織的需求。這些方法均來自于java.util.stream.Collectors
,我們可以稱之為收集器。
3.3.1 歸約
收集器也提供了相應的歸約操作,但是與reduce
在內部實現上是有區別的,收集器更加適用于可變容器上的歸約操作,這些收集器廣義上均基于Collectors.reducing()
實現。
例1:求學生的總人數
long count = students.stream().collect(Collectors.counting());
// 進一步簡化
long count = students.stream().count();
例2:求年齡的最大值和最小值
// 求最大年齡
Optional<Student> olderStudent = students.stream().collect(Collectors.maxBy((s1, s2) -> s1.getAge() - s2.getAge()));
// 進一步簡化
Optional<Student> olderStudent2 = students.stream().collect(Collectors.maxBy(Comparator.comparing(Student::getAge)));
// 求最小年齡
Optional<Student> olderStudent3 = students.stream().collect(Collectors.minBy(Comparator.comparing(Student::getAge)));
例3:求年齡總和
int totalAge4 = students.stream().collect(Collectors.summingInt(Student::getAge));
對應的還有summingLong、summingDouble。
例4:求年齡的平均值
double avgAge = students.stream().collect(Collectors.averagingInt(Student::getAge));
對應的還有averagingLong、averagingDouble。
例5:一次性得到元素個數、總和、均值、最大值、最小值
IntSummaryStatistics statistics = students.stream().collect(Collectors.summarizingInt(Student::getAge));
輸出:
IntSummaryStatistics{count=10, sum=220, min=20, average=22.000000, max=24}
對應的還有summarizingLong、summarizingDouble。
例6:字符串拼接
String names = students.stream().map(Student::getName).collect(Collectors.joining());
// 輸出:孔明伯約玄德云長翼德元直奉孝仲謀魯肅丁奉
String names = students.stream().map(Student::getName).collect(Collectors.joining(", "));
// 輸出:孔明, 伯約, 玄德, 云長, 翼德, 元直, 奉孝, 仲謀, 魯肅, 丁奉
3.3.2 分組
在數據庫操作中,我們可以通過GROUP BY
關鍵字對查詢到的數據進行分組,java8的流式處理也為我們提供了這樣的功能Collectors.groupingBy
來操作集合。比如我們可以按學校對上面的學生進行分組
Map<String, List<Student>> groups = students.stream().collect(Collectors.groupingBy(Student::getSchool));
groupingBy
接收一個分類器Function<? super T, ? extends K> classifier
,我們可以自定義分類器來實現需要的分類效果。上面演示的是一級分組,我們還可以定義多個分類器實現 多級分組,比如我們希望在按學校分組的基礎之上再按照專業進行分組,實現如下
Map<String, Map<String, List<Student>>> groups2 = students.stream().collect(
Collectors.groupingBy(Student::getSchool, // 一級分組,按學校
Collectors.groupingBy(Student::getMajor))); // 二級分組,按專業
實際上在groupingBy
的第二個參數不是只能傳遞groupingBy
,還可以傳遞任意Collector
類型,比如我們可以傳遞一個Collector.counting
,用以統計每個組的個數
Map<String, Long> groups = students.stream().collect(Collectors.groupingBy(Student::getSchool, Collectors.counting()));
如果我們不添加第二個參數,則編譯器會默認幫我們添加一個Collectors.toList()
。
3.3.3 分區
分區可以看做是分組的一種特殊情況,在分區中key
只有兩種情況:true
或false
,目的是將待分區集合按照條件一分為二,Java8的流式處理利用Collectors.partitioningBy()
方法實現分區,該方法接收一個謂詞,例如我們希望將學生分為武大學生和非武大學生,那么可以實現如下
Map<Boolean, List<Student>> partition = students.stream().collect(Collectors.partitioningBy(student -> "武漢大學".equals(student.getSchool())));
分區相對分組的優勢在于,我們可以同時得到兩類結果,在一些應用場景下可以一步得到我們需要的所有結果,比如將數組分為奇數和偶數。以上介紹的所有收集器均實現自接口java.util.stream.Collector
,該接口的定義如下。
public interface Collector<T, A, R> {
/**
* A function that creates and returns a new mutable result container.
*
* @return a function which returns a new, mutable result container
*/
Supplier<A> supplier();
/**
* A function that folds a value into a mutable result container.
*
* @return a function which folds a value into a mutable result container
*/
BiConsumer<A, T> accumulator();
/**
* A function that accepts two partial results and merges them. The
* combiner function may fold state from one argument into the other and
* return that, or may return a new result container.
*
* @return a function which combines two partial results into a combined
* result
*/
BinaryOperator<A> combiner();
/**
* Perform the final transformation from the intermediate accumulation type
* {@code A} to the final result type {@code R}.
*
* <p>If the characteristic {@code IDENTITY_TRANSFORM} is
* set, this function may be presumed to be an identity transform with an
* unchecked cast from {@code A} to {@code R}.
*
* @return a function which transforms the intermediate result to the final
* result
*/
Function<A, R> finisher();
/**
* Returns a {@code Set} of {@code Collector.Characteristics} indicating
* the characteristics of this Collector. This set should be immutable.
*
* @return an immutable set of collector characteristics
*/
Set<Characteristics> characteristics();
}
我們也可以實現該接口來定義自己的收集器。
四、并行流式數據處理
流式處理中的很多都適合采用分而治之的思想,從而在處理集合較大時,極大的提高代碼的性能,Java8的設計者也看到了這一點,所以提供了并行流式處理。上面的例子中我們都是調用stream()方法來啟動流式處理,Java8還提供了parallelStream()
來啟動并行流式處理,parallelStream()
本質上基于Java7的Fork-Join
框架實現,其默認的線程數為宿主機的內核數。
啟動并行流式處理雖然簡單,只需要將stream()
替換成parallelStream()
即可,但既然是并行,就會涉及到多線程安全問題,所以在啟用之前要先確認并行是否值得(并行的效率不一定高于順序執行),另外就是要保證線程安全。此兩項無法保證,那么并行毫無意義,畢竟結果比速度更加重要,以后有時間再來詳細分析一下并行流式數據處理的具體實現和最佳實踐。
五、Stream性能問題
參考資料:Java8 中用法優雅的 Stream,性能也“優雅”嗎?
結論:
對于簡單操作,比如最簡單的遍歷,Stream串行API性能明顯差于顯示迭代,但并行的Stream API能夠發揮多核特性。
對于復雜操作,Stream串行API性能可以和手動實現的效果匹敵,在并行執行時Stream API效果遠超手動實現。
所以,如果出于性能考慮,
- 對于簡單操作推薦使用外部迭代手動實現
- 對于復雜操作,推薦使用Stream API,
- 在多核情況下,推薦使用并行Stream API來發揮多核優勢
- 單核情況下不建議使用并行Stream API
如果出于代碼簡潔性考慮,使用Stream API能夠寫出更短的代碼。即使是從性能方面說,盡可能的使用Stream API也另外一個優勢,那就是只要Java Stream類庫做了升級優化,代碼不用做任何修改就能享受到升級帶來的好處。