關注公眾號(CoderBuff)回復“stream”獲取《Java8 Stream編碼實戰》PDF完整版。
距離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完整版。

