不要再认为Stream可读性不高了!

﹥>﹥吖頭↗ 提交于 2020-04-06 08:48:16

距离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中键的数量。

 1/**
 2 * List列表中的元素是对象类型,使用For循环利用Map的key值不重复通过对象中的学号字段去重,计算有多少学生
 3 * @param students 学生信息
 4 */
 5private void calcStudentCount(List<Student> students) {
 6    Map<Long, Student> map = new HashMap<>();
 7    for (Student student : students) {
 8        map.put(student.getStudentNumber(), student);
 9    }
10  int count = map.keySet().size();
11  System.out.println("List列表中的元素是对象类型,使用For循环利用Map的key值不重复通过对象中的学号字段去重,计算有多少学生:" + count);
12}

你可能会觉得这很简洁清晰,但我要告诉你,这是 错的 !上述代码除了 方法名calcStudentCount 以外,冗余的for循环样板代码无法流畅传达程序员的意图,程序员必须阅读整个循环体才能理解。

接下来我们将使用Stream来准确传达程序员的意图。

Stream中 distinct 方法表示 去重 ,这和MySQL的DISTINCT含义相同。Stream中, distinct 去重是通过通过流元素中的 hashCode()  equals() 方法去除重复元素,如下所示通过 distinct 对List中的String类型元素去重。

 1private void useSimpleDistinct() {
 2    List<String> repeat = new ArrayList<>();
 3    repeat.add("A");
 4    repeat.add("B");
 5    repeat.add("C");
 6    repeat.add("A");
 7    repeat.add("C");
 8
 9    List<String> notRepeating = repeat.stream().distinct().collect(Collectors.toList());
10    System.out.println("List列表中的元素是简单的数据类型:" + notRepeating.size());
11}

再调用完 distinct 方法后,再调用 collect 方法对流进行最后的 计算 ,使它成为一个新的List列表类型。

但在我们的示例中,List中的元素并不是普通的数据类型,而是一个 对象 ,所以我们不能简单的对它做去重,而是要先调用Stream中的 map 方法。

1/**
2 * List列表中的元素是对象类型,使用Stream利用HashMap通过对象中的学号字段去重,计算有多少学生
3 * @param students 学生信息
4 */
5private void useStreamByHashMap(List<Student> students) {
6    long count = students.stream().map(Student::getStudentNumber).distinct().count();
7    System.out.println("List列表中的元素是对象类型,使用Stream利用Map通过对象中的学号字段去重,计算有多少学生:" + count);
8}

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循环实现:

 1/**
 2 * List列表中的元素是对象类型,使用For循环利用Map的key值不重复通过对象中的学号+姓名字段去重,计算有多少学生
 3 * @param students 学生信息
 4 */
 5private void useForByMap(List<Student> students) {
 6    Map<String, Student> map = new HashMap<>();
 7    for (Student student : students) {
 8      map.put(student.getStudentNumber() + student.getStudentName(), student);
 9    }
10    int count = map.keySet().size();
11    System.out.println("List列表中的元素是对象类型,使用For循环利用Map的key值不重复通过对象中的学号+姓名字段去重,计算有多少学生:" + count);
12
13}

如果使用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是如何解决这个问题的:

1/**
2 * 通过Group分组操作
3 * @param students 学生信息
4 * @return 学生信息,key=学号,value=学生信息
5 */
6private Map<Long, List<Student>> useStreamByGroup(List<Student> students) {
7    Map<Long, List<Student>> map = students.stream().collect(Collectors.groupingBy(Student::getStudentNumber));

一行代码搞定分组的场景,这样的代码可读性不高吗?

场景四:过滤分数低于70分的数据,此处“过滤”的含义是排除掉低于70分的数据

传统的for循环样板代码,想都不用想就知道直接在循环体中加入if判断即可:

 1/**
 2 * 通过for循环过滤
 3 * @param  students 学生数据
 4 * @return 过滤后的学生数据
 5 */
 6public List<Student> useFor(List<Student> students) {
 7    List<Student> filterStudents = new ArrayList<>();
 8    for (Student student : students) {
 9        if (student.getScore().compareTo(70.0) > 0) {
10            filterStudents.add(student);
11        }
12    }
13    return filterStudents;
14}

使用Stream流,则需要使用心得操作—— filter 

1/**
2 * 通过Stream的filter过滤操作
3 * @param students 学生数据
4 * @return 过滤后的学生数据
5 */
6public List<Student> useStream(List<Student> students) {
7    List<Student> filter = students.stream().filter(student -> student.getScore().compareTo(70.0) > 0).collect(Collectors.toList());
8    return filter;  

filter 中的Lambda表达式 如果返回true,则包含进此次结果中,如果返回false则排除掉 

以上关于Stream流的操作,你真的还认为Stream的可读性不高吗?

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!