亲宝软件园·资讯

展开

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

OKevin 人气:3
距离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中键的数量。 ```java /** * List列表中的元素是对象类型,使用For循环利用Map的key值不重复通过对象中的学号字段去重,计算有多少学生 * @param students 学生信息 */ private void calcStudentCount(List students) { Map 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类型元素去重。 ```java private void useSimpleDistinct() { List repeat = new ArrayList<>(); repeat.add("A"); repeat.add("B"); repeat.add("C"); repeat.add("A"); repeat.add("C"); List notRepeating = repeat.stream().distinct().collect(Collectors.toList()); System.out.println("List列表中的元素是简单的数据类型:" + notRepeating.size()); } ``` 再调用完```distinct```方法后,再调用```collect```方法对流进行最后的**计算**,使它成为一个新的List列表类型。 但在我们的示例中,List中的元素并不是普通的数据类型,而是一个**对象**,所以我们不能简单的对它做去重,而是要先调用Stream中的```map```方法。 ```java /** * List列表中的元素是对象类型,使用Stream利用HashMap通过对象中的学号字段去重,计算有多少学生 * @param students 学生信息 */ private void useStreamByHashMap(List 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循环实现: ```java /** * List列表中的元素是对象类型,使用For循环利用Map的key值不重复通过对象中的学号+姓名字段去重,计算有多少学生 * @param students 学生信息 */ private void useForByMap(List students) { Map 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表达式: ```java /** * List列表中的元素是对象类型,使用Stream利用HashMap通过对象中的学号+姓名字段去重,计算有多少学生 * @param students 学生信息 */ private void useStreamByHashMap(List 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>,key=学号,value=学生成绩信息 传统的方式仍然可以通过for循环借助Map实现分组: ```java /** * 借助Map通过for循环分类 * @param students 学生信息 */ private Map> useFor(List students) { Map> map = new HashMap<>(); for (Student student : students) { List list = map.get(student.getStudentNumber()); if (list == null) { list = new ArrayList<>(); map.put(student.getStudentNumber(), list); } list.add(student); } return map; } ``` 这种实现比场景一更为复杂,充斥着大量的样板代码,同样需要程序员一行一行读for循环才能理解含义,这样的代码真的可读性高吗? 来看Stream是如何解决这个问题的: ```java /** * 通过Group分组操作 * @param students 学生信息 * @return 学生信息,key=学号,value=学生信息 */ private Map> useStreamByGroup(List students) { Map> map = students.stream().collect(Collectors.groupingBy(Student::getStudentNumber)); return map; } ``` 一行代码搞定分组的场景,这样的代码可读性不高吗? ### 场景四:过滤分数低于70分的数据,此处“过滤”的含义是排除掉低于70分的数据 传统的for循环样板代码,想都不用想就知道直接在循环体中加入if判断即可: ```java /** * 通过for循环过滤 * @param students 学生数据 * @return 过滤后的学生数据 */ public List useFor(List students) { List filterStudents = new ArrayList<>(); for (Student student : students) { if (student.getScore().compareTo(70.0) > 0) { filterStudents.add(student); } } return filterStudents; } ``` 使用Stream流,则需要使用心得操作——```filter```。 ```java /** * 通过Stream的filter过滤操作 * @param students 学生数据 * @return 过滤后的学生数据 */ public List useStream(List students) { List 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完整版。 ![](http://pic-1255645163.cos.ap-chengdu.myqcloud.com/目录.png)
这是一个能给程序员加buff的公众号 (CoderBuff)

加载全部内容

相关教程
猜你喜欢
用户评论