Previously on OOP:
In the last article, we have introduced how to use JUnit to write test cases, so as to perform unit test on the program. Furthermore, one example on GUI has been made.
在今后更加综合性的例子中,test cases还会继续上线,现在会先下线一会儿,因为本黄鸭要举几个比较复杂的Stream的例子。在理论部分和前面的几篇文章中,我们有了一些编写Stream三个部分(source, intermediate and termination)的经验。遗憾的是,这些经验非常基础,不足以支持我们通过本课程的考试,所以各位宝宝们一定要多思考本文中的例子。
在前文中,本黄鸭曾经讲过:一般地,一个project会有一个不包含main函数的main facade,负责被main函数调用,以及统筹安排其他所有的类。因为前面的例子都比较基础,所以没有很好地体现这一点,而在本例中,main facade是StudentStreams class,辅助的类是Course and Student class。
按照top-down的编程思路,我们应该从main facade开始编起,在编写的过程中不断地补充辅助类的功能。相反地,从看懂一个程序的角度上来讲,本黄鸭还是偏向于从辅助类开始看。下面是第一个辅助类,Course class,的代码:
Course类中有两个attributes,名称分别是code和title。
Constructor的功能就是给这两个attributes赋值,没有像GUI的View类那样艰巨的任务。
Methods一共是三个,其中两个是getters,负责取得attributes的值,因为attributes都含有“private”关键字,不能被Course类以外的类访问。最后一个method是toString(),能把一个Course类的实例转化为字符串。
接下来是Student class的代码:
Student类稍微比Course复杂一点。在attributes的声明之前,先定义了一个共有的enumerate,翻译成中文可以是:枚举型变量,名称是Gender。所谓枚举型,就是在声明的时候定好几个值,然后凡是这种枚举型的变量,一律只能取规定好的值中的一个。现在Gender规定了两个值,一个是F,另外一个是M。那么凡是Gender类型的变量,只能取F或者是M,像Unknown, Female, transgender等等,都是不行的。
还定义了两个Gender类型的常数,F和M,值分别是Gender.F和Gender.M。之所以这两个是常数不是变量,是因为声明中含有“final”关键字。
Student类要记录的信息分有学生信息和选课信息,学生信息由id,first, last, gender attribute来记录;而选课信息都在courses中,这个attribute的类型是Collection,也就是一个Collection,里面存放的多个Course类型的object references。
在Constructor中,先把参数的值分别赋予对应学生信息的attributes。然后再创建一个LinkedList的实例,把object reference的值赋予coursesattribute。在前文中,我们推荐的做法是把object reference的声明和object的创建写在一行中,以防漏掉,即:
直接把上面一行代码写在attributes的位置,并且不在constructor中创建LinkedList的实例,也是没有问题的。
前六个methods都是getters,第七个是toString(),最后两个是和courses相关的。enrolledIn()函数返回courses,即整个LinkedList的object reference。enroll()函数能把参数中收到的Course实例添加到courses里面去。笼统地讲,这两个函数是courses的getter and setter。
有了Course类和Student类之后,我们可以进入main facade,即StudentStreams类,的编写。
StudentStreams类中只有一个attribute,名字叫做students,类型是List,也就是存有多个Student类的数据的List。这个List的实例的创建在constructor中。
接下来,constructor做了一件大事,就是创建了几个Student和Course的实例,并且用Stream的方法指定某些学生去参加某些课程的学习。本黄鸭觉得本段代码中的Stream用法非常基础,不需要全部都详细地解说一遍,于是我们就挑最后一个分析一下吧:
(1)stream():把studentsLinkedList转化为一个Stream。
(2)skip(2):跳过最前面的两个元素,即John Smith和Mary Johnson跳过。
(3)limit(3):取最前面的三个元素,即Andrea Rossi,Giulia Ferrari,和Wei Wang。
(4)forEach(s -> s.enroll(c3)):对于Andrea Rossi,Giulia Ferrari,和Wei Wang这三个人,调用enroll()函数,让他们参加c3课程的学习。这里的Lambda expression不是在创建某一个functional Interface的子类的实例+重载abstract函数,而是调用。类似的还有:
第二个constructor非常简单。如果从参数那里收到了存放在List中的好多Student类型的数据,那么就直接给本类的attribute赋值。
下面开始按照需求来编写methods。
在本段代码中,有一个陌生的函数是parallel(),它的作用是开始并行处理。Stream出场自带pipe-lining功能,已经很高效了,加上并行功能,无疑是要接近神速,那么是不是所有的Stream都要加上parallel()函数呢?答案是否定的,原因如下:
(1)并行不一定更快。把并行的结果合并,给并行的线程分配任务,分配资源,等等;如果在这些任务上耗费了大量的时间,并行后,效率反而会降低。
(2)本课程的学习重点在于编程,不在于大数据的分析方法,所以提供的数据一般都会比较少,那么用与不用并行处理花费的时间差不多。
本黄鸭还需要再强调一遍,对于filter()函数,符合参数条件的数据会保留在Stream中,不符合的会被踢掉。在本段代码中,filter()的参数是一个method reference,表示调用。
然后,collect()函数调用了predefined collectors中的一个,toList(),把Stream的数据收集在一个List中。这个函数的返回值是Collection类型的,所以collector生成的List可以被直接返回。
最后,Stream能实现的功能必然能用非Stream的办法来解决:
这个需求和上一个一样,可以通过用filter() + collect()函数来实现。区别在于本段代码把filter()的参数,写成了一个Predicate的object的形式。这个object的名字叫做isNamedJohn。
此外,不用collector的话,forEach()也能实现这个需求:
用forEach()把每一个Stream的数据加入到一个名叫res的LinkedList中去。但是如果调用了parallel()函数,使用并行执行的话,那么这种做法会有一定的副作用,原先students中的数据的顺序被打乱。
因为并行的时候是几个线程各取一定量的数据,再分别执行termination process前面的步骤。那么哪个线程结束得早,哪个线程结束得完,全凭Java virtual machine的Scheduler的心情,开发者是无法控制的。所以合并到res中的顺序很可能不是原先students中的顺序。
顺序被打乱以后,调用sort()函数可以排序,可惜这个操作是非线性的,再好的算法都只能使得复杂度降到log(n),n是待排序元素的个数。像sort()这样的非线性操作,我们应该能少用就少用。
欲知后事如何,且听下回分解。
欢迎使用本黄鸭编写的小程序~
微信公众号二维码:
领取专属 10元无门槛券
私享最新 技术干货