距離Java 8發佈已經過去了7、8年的時間,Java 14也剛剛發佈。Java 8中關於函數式編程和新增的Stream流API至今飽受“爭議”。 如果你不曾使用Stream流,那麼當你見到Stream操作時一定對它發出過鄙夷的聲音,併在心裡說出“這都寫的什麼玩意兒”。 如果你熱衷於使用Stream ...
距離Java 8發佈已經過去了7、8年的時間,Java 14也剛剛發佈。Java 8中關於函數式編程和新增的Stream流API至今飽受“爭議”。
如果你不曾使用Stream流,那麼當你見到Stream操作時一定對它發出過鄙夷的聲音,併在心裡說出“這都寫的什麼玩意兒”。
如果你熱衷於使用Stream流,那麼你一定被其他人說過它可讀性不高,甚至在codereview時被要求改用for迴圈操作,更甚至被寫入公司不規範編碼中的案例。
這篇文章將告訴你,不要再簡單地認為Stream可讀性不高了!
下麵我將圍繞以下舉例數據說明。
這裡有一些學生課程成績的數據,包含了學號、姓名、科目和成績,一個學生會包含多條不同科目的數據。
ID | 學號 | 姓名 | 科目 | 成績 |
---|---|---|---|---|
1 | 20200001 | Kevin | 語文 | 90 |
2 | 20200002 | 張三 | 語文 | 91 |
3 | 20200001 | Kevin | 數學 | 99 |
4 | 20200003 | 李四 | 語文 | 76 |
5 | 20200003 | 李四 | 數學 | 71 |
6 | 20200001 | Kevin | 英語 | 68 |
7 | 20200002 | 張三 | 數學 | 88 |
8 | 20200003 | 張三 | 英語 | 87 |
9 | 20200002 | 李四 | 英語 | 60 |
場景一:通過學號,計算一共有多少個學生?
通過學號對數據去重,如果在不藉助Stream以及第三方框架的情況下,應該能想到通過Map的key鍵不能重覆的特性迴圈遍曆數據,最後計算Map中鍵的數量。
/**
* List列表中的元素是對象類型,使用For迴圈利用Map的key值不重覆通過對象中的學號欄位去重,計算有多少學生
* @param students 學生信息
*/
private void calcStudentCount(List<Student> students) {
Map<Long, Student> map = new HashMap<>();
for (Student student : students) {
map.put(student.getStudentNumber(), student);
}
int count = map.keySet().size();
System.out.println("List列表中的元素是對象類型,使用For迴圈利用Map的key值不重覆通過對象中的學號欄位去重,計算有多少學生:" + count);
}
你可能會覺得這很簡潔清晰,但我要告訴你,這是錯的!上述代碼除了方法名calcStudentCount以外,冗餘的for迴圈樣板代碼無法流暢傳達程式員的意圖,程式員必須閱讀整個迴圈體才能理解。
接下來我們將使用Stream來準確傳達程式員的意圖。
Stream中distinct
方法表示去重,這和MySQL的DISTINCT含義相同。Stream中,distinct
去重是通過通過流元素中的hashCode()
和equals()
方法去除重覆元素,如下所示通過distinct
對List中的String類型元素去重。
private void useSimpleDistinct() {
List<String> repeat = new ArrayList<>();
repeat.add("A");
repeat.add("B");
repeat.add("C");
repeat.add("A");
repeat.add("C");
List<String> notRepeating = repeat.stream().distinct().collect(Collectors.toList());
System.out.println("List列表中的元素是簡單的數據類型:" + notRepeating.size());
}
再調用完distinct
方法後,再調用collect
方法對流進行最後的計算,使它成為一個新的List列表類型。
但在我們的示例中,List中的元素並不是普通的數據類型,而是一個對象,所以我們不能簡單的對它做去重,而是要先調用Stream中的map
方法。
/**
* List列表中的元素是對象類型,使用Stream利用HashMap通過對象中的學號欄位去重,計算有多少學生
* @param students 學生信息
*/
private void useStreamByHashMap(List<Student> students) {
long count = students.stream().map(Student::getStudentNumber).distinct().count();
System.out.println("List列表中的元素是對象類型,使用Stream利用Map通過對象中的學號欄位去重,計算有多少學生:" + count);
}
Stream中的map
方法不能簡單的和Java中的Map結構對應,準確來講,應該把Stream中的map操作理解為一個動詞,含義是歸類。既然是歸類,那麼它就會將屬於同一個類型的元素化為一類,學號相同的學生自然是屬於一類,所以使用map(Student::getStudentNumber)
將學號相同的歸為一類。在通過map
方法重新生成一個流過後,此時再使用distinct
中間操作對流中元素的hashCode()
和equals()
比較去除重覆元素。
另外需要註意的是,使用Stream流往往伴隨Lambda操作,有關Lambda並不是本章的重點,在這個例子中使用
map
操作時使用了Lambda操作中的“方法引用”——Student::getStudentNumber,語法格式為“ClassName::methodName”,完整語法是“student -> student.getStudentNumber()”,它表示在需要的時候才會調用,此處代表的是通過調用Student對象中的getStudentNumber方法進行歸類。
場景二:通過學號+姓名,計算一共有多少個學生?
傳統的方式依然是藉助Map數據結構中key鍵的特性+for迴圈實現:
/**
* List列表中的元素是對象類型,使用For迴圈利用Map的key值不重覆通過對象中的學號+姓名欄位去重,計算有多少學生
* @param students 學生信息
*/
private void useForByMap(List<Student> students) {
Map<String, Student> map = new HashMap<>();
for (Student student : students) {
map.put(student.getStudentNumber() + student.getStudentName(), student);
}
int count = map.keySet().size();
System.out.println("List列表中的元素是對象類型,使用For迴圈利用Map的key值不重覆通過對象中的學號+姓名欄位去重,計算有多少學生:" + count);
}
如果使用Stream流改動點只是map操作中的Lambda表達式:
/**
* List列表中的元素是對象類型,使用Stream利用HashMap通過對象中的學號+姓名欄位去重,計算有多少學生
* @param students 學生信息
*/
private void useStreamByHashMap(List<Student> students) {
long count = students.stream().map(student -> (student.getStudentNumber() + student.getStudentName())).distinct().count();
System.out.println("List列表中的元素是對象類型,使用Stream利用Map通過對象中的學號+姓名欄位去重,計算有多少學生:" + count);
}
前面已經提到在使用map時,如果只需要調用一個方法則可以使用Lambda表達式中的“方法引用”,但這裡需要調用兩個方法,所以只好使用Lambda表達式的完整語法“student -> (student.getStudentNumber() + student.getStudentName())”。
這個場景主要是熟悉Lambda表達式。
場景三:通過學號對學生進行分組,例如:Map<Long, List>,key=學號,value=學生成績信息
傳統的方式仍然可以通過for迴圈藉助Map實現分組:
/**
* 藉助Map通過for迴圈分類
* @param students 學生信息
*/
private Map<Long, List<Student>> useFor(List<Student> students) {
Map<Long, List<Student>> map = new HashMap<>();
for (Student student : students) {
List<Student> list = map.get(student.getStudentNumber());
if (list == null) {
list = new ArrayList<>();
map.put(student.getStudentNumber(), list);
}
list.add(student);
}
return map;
}
這種實現比場景一更為複雜,充斥著大量的樣板代碼,同樣需要程式員一行一行讀for迴圈才能理解含義,這樣的代碼真的可讀性高嗎?
來看Stream是如何解決這個問題的:
/**
* 通過Group分組操作
* @param students 學生信息
* @return 學生信息,key=學號,value=學生信息
*/
private Map<Long, List<Student>> useStreamByGroup(List<Student> students) {
Map<Long, List<Student>> map = students.stream().collect(Collectors.groupingBy(Student::getStudentNumber));
return map;
}
一行代碼搞定分組的場景,這樣的代碼可讀性不高嗎?
場景四:過濾分數低於70分的數據,此處“過濾”的含義是排除掉低於70分的數據
傳統的for迴圈樣板代碼,想都不用想就知道直接在迴圈體中加入if判斷即可:
/**
* 通過for迴圈過濾
* @param students 學生數據
* @return 過濾後的學生數據
*/
public List<Student> useFor(List<Student> students) {
List<Student> filterStudents = new ArrayList<>();
for (Student student : students) {
if (student.getScore().compareTo(70.0) > 0) {
filterStudents.add(student);
}
}
return filterStudents;
}
使用Stream流,則需要使用心得操作——filter
。
/**
* 通過Stream的filter過濾操作
* @param students 學生數據
* @return 過濾後的學生數據
*/
public List<Student> useStream(List<Student> students) {
List<Student> filter = students.stream().filter(student -> student.getScore().compareTo(70.0) > 0).collect(Collectors.toList());
return filter;
}
filter
中的Lambda表達式如果返回true,則包含進此次結果中,如果返回false則排除掉。
以上關於Stream流的操作,你真的還認為Stream的可讀性不高嗎?
關註公眾號(CoderBuff)回覆“stream”獲取《Java8 Stream編碼實戰》PDF完整版。
這是一個能給程式員加buff的公眾號 (CoderBuff)