首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

面向对象的编程-Application 39

Previously on OOP:

Observable and Observer Interfaces are to be used in pairs. Their functionality is to realize communication decoupling. Therefore, the communication among MVC components of a GUI can be decoupled using these Interfaces.

Stream是一种用pipe-lining来批量处理数据的方法。I/O Stream,顾名思义,是Stream的一种,用来处理从文件或者其他地方的输入和输出。在读取文件的时候,我们既可以按照字节来读,又可以一行一行地读取。如何在读到的一行中提取我们需要的字段呢?有以下几种方式:

(1)数字符的个数来划分字段。比如:20181128,前面4个字符是年份,后面两个字符是月份,最后两个字符是日子。

(2)按照文件中固有的分隔符来划分字段。比如:2018-11,按照短横线分割成前后两个部分,前面的是年份,后面的是月份。

(3)使用regular expression,这是一种用符号语言,可以告诉编译器字符串的结构是怎样的。比如:2018/11/28,写成regular expression可以是“%4d / %2d / %2d”,编译器在读取的时候,会把第一个斜线前面的四位整形数据作为年份,第二个前面的两位整形数据作为月份,最后两位整形数据作为日子。

在本文中,本黄鸭会举一些regular expression运用在Stream中的例子。需要读取的文件如下:

空格可以看做是两个字段的分隔符,前面是学生的名字,后面是学生的成绩。这和前文中出现过的例子基本是一模一样的,只是分隔符从横线变成了空格。

在Student类中,有两个attributes,分别存放从文件中读取到的两个字段。构造函数的任务在创建实例的时候给两个attributes赋值。Methods的前两个是getters,第三个是toString()函数。

另外,还有一个methods是newStudent()。因为在使用split(“”)函数把读到的文件的一行按照空格字符划分成前后两个字段的时候,会把拆分后的字段存放在String类型的数组中。那么,我们在创建实例的时候,就不能调用前一段代码的构造函数,而应该再编写一个接收String类型数组为参数的构造函数,或者调用newStudent()方法。

在newStudent()方法中,数组的第号元素是学生的名字,是字符串类型的,所以可以直接使用。而数组的第1号元素是学生的成绩,应该是double类型的,所以需要类型转换之后才能使用。最后,调用了Student类的构造函数,创建实例,并返回。

总之,如果在读取文件的时候使用split(“”)函数,那么肯定会调用到newStudent()方法。

这是StudentCollection类的main函数。先用readMarksStream(),readMarks(),或者readMarksStreamRE()来读取“marks.txt”文件,读到Collection类型的数据结构中。

然后,用FileWriter把该数据结构写到输出文件中去,结果如下:

从这个结果中,我们可以看出,student的内层数据结构,即Student,肯定是调用了重载后的toString()函数。外层数据结构,即Collection,先被类型转化到Array,然后调用了Array类中的toString()函数。

在以上两个过程中,可能会出现的run-time Exception可能有:

(1)IOException:读写文件时会出现,比如文件打不开,等等。

(2)NumberFormatException:调用readMarksStream(),readMarks(),或者readMarksStreamRE()时,把第二个字段从String转化为double类型可能会出现。

Solution 1

readMarksStream()是第一个能读写“marks.txt”文件的函数,做法和前文的例子完全相同。用BufferedReader把文件打开,然后按照行读取,并且转化为Stream。接着,用split(" ")函数把每一行按照空格拆开,分成前后两个字段,储存在String类型的数组里,此时Stream的类型是Stream。再调用Student类的newStudent()函数创建Student类型的实例,此时类型变为Stream。最后把Stream的元素收集在List中,并返回。

如果没有编写newStudent()函数,还可以:

(1)在Student类中,编写一个接收String[]类型的参数的构造函数。

(2)结合forEach和非Stream的方法,如下所示:

Solution 2

第二种做法是非Stream的做法。先声明三个local variable,分别是用于打开文件的BufferedReader,存放返回值的LinkedList,还有存放文件每行字符串的String。

接下来使用while循环来按行读取文件,这里需要纠结一小下:try-catch block应该放在while循环结构里面还是外面。如果放在里面,那么当某一次循环出现Exception的时候,会执行catch block,循环不会退出;相比之下,放在外面的时候,会执行catch block,并且退出循环结构。为了能尽可能读取到更多的数据,我们选择外层是while循环,内层是try-catch block。

当分割出来的第二个字段不是double类型的分数之时,会出现NumberFormatException,进入catch block。如果没有Exception,那么就创建Student类型的实例,并且加入到存放返回值的LinkedList中。

Solution 3

第三种做法是本文的重点,regular expression。Pattern中编写的是regular expression,即用符号语言表示的字符串的格式。本段代码中的Pattern非常复杂,我们可以选用简单一些的,比如“(*)\s(*)”。

第二个是Matcher,存放的是与Pattern格式比对过的字符串。Matcher实例的创建不是调用Matcher类的构造函数,而是调用Pattern中的matcher()函数,看字符串是否和Pattern相符。如果文件中的每一行都符合Pattern,那么皆大欢喜,否则就使用find()函数找出符合Pattern的最长的子字符串。

每一组定义在Pattern中的括号,就是一个capture group,另外,第零号capture group是整个Pattern。所以第一个capture group是学生姓名,第二个是分数。

最后,创建Student的实例,收集在List中,并且返回。

Solution 4

最后一种方法综合了Stream和Scanner。所谓Scanner,可以看做是一个parser,翻译成中文可以是转换器。它能靠regular expression / split()函数来读取primitive type和String。在代码编写方面,和Iterator比较相似,使用hasNext() and next()来判定有没有下一个元素,以及移动到下一个元素的位置上去。

在本段代码中,Scanner使用useDelimiter()把每行字符串拆分成两个字段,而不是regular expression。那么,使用regular expression应该怎么编写呢?这是本黄鸭留给各位宝宝们的思考题。

欢迎使用本黄鸭编写的小程序~

微信公众号二维码:

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181128G1840400?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券