Previously on OOP:
FileReaderandFileWriterclass can be used to read a file composed of characters. And there are three ways: (1), read one character at a time; (2), read a buffer's size at a time; (3), read one line at a time.
在上文中,我们提到过可以在读取“marks.txt”的时候使用regular expression来把不同的字段存放到相应的数据结构中。
由于“marks.txt”的结构还算比较简单,每行只有两个字段,中间用“-”分割,所以我们也可以选用比regular expression简单的分割方式。本黄鸭将要在本文中举一个这样的例子。
首先,我们要选定一种数据结构来存放分解出来的字段。下面是一种在本课程中最为普遍的结构:
数据结构分有内外两层。
外层选用数组,Collection,或者是Map中的任意一种。输入文件中的每一行就存为外层数据结构中的一个数据,或者说是一个entry。
内层是开发者新创建的一个类,比如Student,里面有两个attributes,分别存放当前这个学生的名字和分数。
因为编程推荐使用top-down的思维方式,即从大的框架上想起,再慢慢补充实现的细节,这样一来,许多细节不用自己编写,能选择编译器给出的几个建议中的一个,所以从编写顺序上来看,应该先编写外层数据结构。但是由于内层的实现比较简单,本黄鸭还是选择先看内层数据结构:
Student类的attributes, constructor, and getter methods都应该没有问题。toString()方法可以把Student的实例转化为字符串,方便于打印。
最后一个newStudent()函数是要和read by line+split according to“-”配合使用的。在split according to“-”之后,“-”的前后两个字段会自动被保存在一个数组中。然后这个数组作为参数传递到newStudent()函数中,进行类型转换,最后调用constructor,创建Student类的实例。
我们再看外层数据结构:
main函数下面第一行,“Collection”是囊括了两层数据结构的声明。外层是Collection或者是它的子类,内层是Student,也就是说一个Collection里面存放的数据类型是Student类的。本黄鸭还要再强调一遍,内层一定不能是Student类的子类,因为Java Generics的type invariant性质。
“Collection”数据结构的object reference的名称是“student”,这个名字不是特别好。最好改成复数形式,体现里面存放了复数个Student类的数据。
readMarks()是本类中的一个方法,下文中会作为重点分析。但是对于这个方法有两点是很明确的:
(1)返回值:肯定是Collection的实例。
(2)功能:读取“marks.txt”文件的内容,并且把各种字段分门别类地存放到数据结构中去。
本段代码中还包含了try block,是为了handleIOException。这里编写得不是很完整,缺少catch block。这是因为main函数位于hierarchy of invocation的顶端,不能把任何的Exception传递出去,只能当场解决。
接下来,在把数据写入output file之后,没有关闭文件,反而调用了flush()函数。这种做法也是可以的,因为该函数的功能是:Write everything from the cache to the secondary memory before the program terminates。
P.S.
Student类中重载了toString()函数,所以单个Student的打印结果肯定是按照重载后的来。但是Collection / List的toString()函数没有重载,所以整个student Collection的打印还是按照原来的来。
下面是本文的重点,implementation ofreadMarks() method,有以下两种方案可供选用。
Solution 1
readMarks()方法的返回值的类型是“List”,符合要求。BufferedReader类在上一篇文章中已经举过例子了,它能实现按照行来读取的文件内容。接下来,代码中声明一个局部的、临时的数据结构,类型是“LinkedList”,名称是“res”,顾名思义,会用作为返回值。还声明了一个String类型的变量,名字叫做“line”,用于存放从“marks.txt”中读取的每一行。
然后,使用while循环结构依次读取并处理“marks.txt”文件中的每一行。String类中有一个函数是split(),能把指定字符的前面和后面部分差分成sub-strings,并且存放到一个叫做“elements”的数组中。split()函数的指定字符通过参数传递。
再把elements数组作为参数,调用Student类中的newStudent()函数。这里使用dotted notation来调用,在dotted notation前面不是Student类的实例,而是类的名称,说明newStudent()函数必须是static的。
While循环体的最后一行把新建的Student的实例加入到res数据结构中。在这个函数的最后,返回res。
Solution 2
第二种做法就是用Stream。Open Stream的方法比较特别,调用了BufferedReader类的lines()函数。
第一个intermediate process是用Lambda expression来调用String类的split()函数,“line”是一个String类型的临时变量。之所以用Lambda expression是因为split()函数有参数需要传递。
比较特别的是这个Lambda expression外面套着一个map()函数。为什么是map()函数呢?因为在open Stream执行之后,Stream中的数据是“marks.txt”中的每一行,所以都是String类型的。而在split()函数调用之后,Stream的数据都要变成存放sub-strings的数组。也就是说,第一个intermediate process会导致Stream中数据的类型发生变化。那么,像filter()这些不会发生数据类型变化的肯定就不行了,只能在flatMap() and map()中选。所谓flatMap(),是把原来Stream的数据中的数据结构抽出来,整合成一个新的Stream,不符合现在的使用场景,所以就只能选用map()函数了。
第二个intermediate process是用method reference来调用Student类的newStudent函数。
最后的termination process采用了已经定义在库类中的Collector,名称是“toList()”,这个Collector能把Stream的所有数据收集起来,形成一个List。如果不用Collector,用forEach loop也能实现这个功能,有兴趣的宝宝们可以自己练习一下。
欢迎使用本黄鸭编写的小程序~
微信公众号二维码:
领取专属 10元无门槛券
私享最新 技术干货