不要再認為Stream可讀性不高了!


關注公眾號(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完整版。

這是一個能給程序員加buff的公眾號 (CoderBuff)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM