软工实践寒假作业(2/2)
寒假作业(2/2)
这个作业属于哪个课程 | 2021春软件工程实践|W班(福州大学) |
---|---|
这个作业要求在哪里 | 作业要求 |
这个作业的目标 | 1. 重读《构建之法》并提出问题与思考 2. 通过开发词频统计程序学习使用github与软件测试 |
其他参考文献 | CSDN、简书、JAVA性能优化:35个小细节让你提升代码的运行效率 |
任务一
一、阅读《构建之法》并提出问题
- 我看了第四章 两人合作 4.3.2 goto的这一段文字
函数最好有单一的出口,为了达到这一目的,可以使用goto。只要有助于程序逻
辑的清晰体现,什么方法都可以使用,包括goto,--p69
有这个问题:以往的课程里有提到不提倡使用goto,也从汇编语言的角度学习过它,goto语句不受限制的跳转,风险很大,应该是弊大于利。所以在实践中究竟真相为何?
我查了资料,有这些说法:
在C/C++等高级编程语言中保留了goto语句,但被建议不用或少用,因为其代码的可读性和执行效率较差,可以被其他写法代替。在一些更新的高级编程语言,如Java不提供goto语句,它虽然指定goto作为关键字,但不支持它的使用,使程序简洁易读;尽管如此后来的c#还是支持goto语句的,goto语句一个好处就是可以保证程序存在唯一的出口,避免了过于庞大的if嵌套。
根据我的实践,我得到这些经验:
虽然程序可能不需要goto也有足够的表达能力,但在一些情况下(比如必须写多重嵌套循环的情况)goto就很简明了,比一般的方法要容易跳出多层循环,在正确使用的情况下可以为我们带来便利。
但是我还是不太懂,我的困惑是:
在大的项目中是否应避免使用goto语句?
2. 我看了第六章 敏捷流程 6.5 敏捷的问答的这一段文字
问:那比较有名的最佳实践是什么?
答:这就太多了,你把任意三个字母组合一下,说不定就是一个最佳实践,例如
TDD(踢弟弟,TestDriven Development)就是一个最佳实践。很多程序员老大
哥都喜欢踢弟弟。--p120
有这个问题:第四章结对编程的介绍里也有提到“领航员也可以设计TDD中的测试用例”。所以TDD是什么?测试驱动?书中可能有简要介绍但可能漏看了,总之第一次接触这个名词。
我查了资料,有这些说法:
TDD 是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD 是 XP(Extreme Programming)的核心实践。它的主要推动者是 Kent Beck。
根据所查阅资料,我得到这些启发:
个人理解总结一下就是:将需求(测试样例)作为一种开发手段,而不是目标。上图提到的传统编码方式真是太有既视感了!尽管还只是学生但也很有体会了。而TDD通过明确的流程,任务分解、列Example,让我们一次只关注一个点,思维负担更小。且先写测试可以帮助我们去思考需求,并提前澄清需求细节,而不是代码写到一半才发现不明确的需求。
但是我还是不太懂,我的困惑是:
如何做任务分解?第一步如何开始,实践经历较少,脑袋中有点小空白。
网络上给出的推荐指向这一系列文章:编程的精进之法与《像机器一样思考》,值得学习。
3. 关于PSP表格的疑问
对于在本次作业中第一次接触到的PSP表格,我的疑问是:如何对表格内涉及到的时间做预估与统计?有什么推荐的工具或方法吗?难道要去查看程序使用历史时间?我不得不诚实地向老师与助教们坦白:我这次作业里的PSP表格部分内容是脑补出来的,因为我只清楚我查找资料并学习,编写代码(开发),与测试修改分别大致花了多少时间,这部分填入的时间较准确,其他的部分似乎都杂糅在一起,我实在不知道如何计算,预估的时候其实心里也没有个底,不是很熟悉各个过程究竟要花、会花多少时间。
因为这第一次个人作业的设计需求,与团队多人的大项目相比,较为简单;不少工具也是第一次学习使用,无效的耗时长;且在翻看往届学长的博客时,发现很多人对表格中的填写内容也有歧义与误区,就比如“Estimate估计这个任务需要多少时间”,很多人填的是“完成任务需要的预估总时间”,但其实翻看《构建之法》会发现这一栏要填的是“估计”这个动作花了多少时间,可见大家对其的陌生。所以这个PSP表格其实,在这次作业中没有很大的参考价值,只是先学习并了解了其中涉及到的名词、内容。
所以个人的拙见是,在下一届的教学时,老师和助教们可以酌情删除类似“第一次个人作业"这种实践中填写PSP表格的要求,暂时只做了解学习即可,等到后续复杂的大作业再做实践。
4. 我看了第十六章 IT行业的创新 16.1.2 迷思之二:大家都喜欢创新的这一段文字
不但大众不喜欢创新,甚至连创新者自己都不例外,有些创新者甚至恨创新。。。不但一般民众不喜欢创新,有时候,连IT行业的技术人员都不喜欢新东西。--p342
有这个问题:此种说法被作者反复提及,但似乎有失偏颇,该段落后文也提到了关于颠覆式创新的故事,我们应该说,大众不喜欢的是颠覆,而非创新本身。
我查了资料,有这些说法:
1997年,哈佛大学商学院教授克里顿·克里斯滕森在《创新者的窘境:当新技术使大公司破产》一书中正式提出颠覆性创新的概念:“颠覆性创新是一种另辟蹊径、会对已有传统或主流技术产生颠覆性效果的技术创新和商业模式创新。”
颠覆性创新的特征:1.破坏性或变革性;2.顾客价值导向性;3.不确定性;4.突出的财富杠杆效应。当下的计算机行业似乎同时存在着两种场景:领先企业总是能赢,无人能胜,而另一种情况是,在位企业输给新兴企业,虎落平阳。
根据所查阅资料,我得到这些启发:
对于“创新”的喜欢与不喜欢,更应该倾向于说他是一种“选择”,做加法类型的锦上添花式的创新,相信大众是喜闻乐见的;而颠覆却是上来就把桌子给掀了,自己的生活要做出改变,“懒惰”的大众自然是没法一下子接受的。而对于企业来说,则是走在平稳前行和冒险革新的分岔路口,这也许并非能用“喜欢与不喜欢”简单概括,而是有没有胆量去做出选择,有没有发现机遇的长远目光和资本、运气方面的问题。
5. 我看了第十七章 人,绩效和职业道德 17.8的这一段文字
原则1 公众:软件工程师的行为应与公众利益一致。
原则2 客户与雇主:软件工程师以其客户和雇主利益最大化的方式做事,与公众利益保持一致。--p406
有这个问题:这两个原则似乎有矛盾,当遇到道德冲突时,其中的权衡将难以做到。
我查了资料,有这些说法:
在工作中,考虑到盈利的要求、工作中的压力等等因素,我们可能会遇到明知入手的项目不符合伦理道德,但上级领导强制要求你完成的情况。若拒绝完成,将面临丢掉工作;若接收项目,则需要面临风险。尽管近80%的开发人员认为他们确实需要考虑代码的道德含义,但58%的人认为高层管理人员对软件负有最终责任。问题是,从法律的角度来看,这些开发者可能是错的。公司、企业、事业单位、机关、团体为单位谋取利益,经单位决策机构或者负责人决定实施的,法律规定应当负刑事责任的危害社会的行为称为单位犯罪。我国刑法对单位犯罪原则上采取双罚制度,即单位犯罪的,对单位判处罚金,并对其直接负责的主管人员和其他直接责任人员判处刑罚。若是接受了项目,就将是违法的开始。
在个人开发中,可能是接的外包,或者是制作的小工具,由于是独立开发者,缺少充分的项目审核,开发过程完全由自己决定,因此更容易遇到伦理问题,甚至违反法律。
根据所查阅资料,我得到这些启发:
面对这类问题,显然我们不能采取教条主义,而应当进行全面的思考。软件产品很难顾及到所有人的利益,往往对公众利益的利与弊是相伴而生的,关键是看它的利是否大于弊及社会接受度。如淘宝的出现,让大量实体商家受挫甚至倒闭,这似乎违背了公众利益。但它却又衍生出了电商、快递行业,给予了更多能抓住机遇的人工作的机会,显然淘宝往好的方向改变了人们的生活方式,它的利是大于弊的,因此被社会接受。
但是我还是不太懂,我的困惑是:
以上的例子只能说明“软件”与“就业”方面的冲突,大数据时代背景下,隐私数据的获取同样是个很火热的话题,例如当我在现实中说话提及一个物品时,打开淘宝常常就会给我推送这个商品,这种细思极恐的“功能”,虽然现在的app在你刚下载安装时会提到说“你是否同意我们进行数据采集,启用cookie权限。。。”等,初衷往往是改善用户体验,但最终可能会有“Facebook泄露隐私门”的结局。再如类似“赌博”的抽卡类游戏,相关法律并不完善,玩家一掷千金,但可能已经中了开发者的“概率”圈套。我的疑问是软件开发人员该如何看待这种会侵犯公众利益的“不透明,不公开”的“功能”或者说代码?我们是否应增加他的透明度?
任务二
一、Github仓库地址
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
•Planning | 计划 | 60 | 60 |
Estimate | 估计这个任务需要多少时间 | 60 | 60 |
•Development | 开发 | 1350 | 1700 |
Analysis | 需求分析 (包括学习新技术) | 120 | 120 |
Design Spec | 生成设计文档 | 30 | 10 |
Design Review | 设计复审 | 30 | 10 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
Design | 具体设计 | 60 | 30 |
Coding | 具体编码 | 900 | 1080 |
Code Review | 代码复审 | 180 | 60 |
Test | 测试(自我测试,修改代码,提交修改) | 120 | 360 |
•Reporting | 报告 | 270 | 290 |
Test Report | 测试报告 | 90 | 120 |
Size Measurement | 计算工作量 | 30 | 20 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 150 | 60 |
合计 | 1680 | 2050 |
三、解题思路
流程图
从文件读取,输入英文文本(只考虑Ascii码,汉字不需考虑)
先通过某种方法读取文件并进行预处理,文本文件→字符/字节流,不同方法性能有差距。
字符/字节流→字符串content,通过toLowerCase()方法将大写全转换为小写。
这同时解决了后续统计单词与输出频度列表时,不区分大小写/输出小写问题。
1. 统计字符数(对应输出第一行,空格,水平制表符,换行符,均算字符)
根据作业要求,返回字符串的.length()即可。
2. 统计文件的有效行数(对应输出第三行):任何包含非空白字符的行,都需要统计。
先想到的是通过readline()读取字符串,维护一个临时变量用于计数,equals("")匹配到就加一即可,顺便按行切分,但是这样有性能一般、遇到空白符扎堆出现会计数错误等问题。
最终是利用正则表达式匹配有效行,空行不统计:包括\t \r \n 空格以及由他们组成的情况。
3.统计文件的单词总数(对应输出第二行),单词:至少以4个英文字母开头,跟上字母数字符号,单词以分隔符分割,不区分大小写。
先通过某种方法切分字符串,再过滤掉不符合格式的单词,不同方法性能有差距。
先想到的是利用StreamApi中.split()切分和词法分析中的自动机组合在一起,统计符合要求格式的单词及其频度,得到一个HashMap变量words。
后来发现还可以使用StringTokenizer搭配正则表达式,逐个切分、匹配、计入有效单词。
最后返回HashMap的.size()即可。
4. 统计文件中各单词的出现次数(对应输出接下来10行),最终只输出频率最高的10个。(值降序,值相同的键字典序靠前的优先)
将上一步中得到的单词频度HashMap通过StreamApi排序,返回一个用于输出的频度列表。
按以上顺序逐行打印至输出文本,输出的格式如下
characters: number
words: number
lines: number
word1: number
word2: number
...
思路:在一个FilePrinter类中,writeFile方法接收传来的待打印数值和频度列表,使用BufferedWriter按要求格式高效输出。
四、代码规范
五、设计与实现过程
类设计
CharAndWordCounter
方法(1)countChar
功能:计算字符数
输入:已经过预处理的字符串
返回:字符串总字符数
方法(2)countWord
功能:计算单词数
输入:已填入频度计数的HashMap
返回:总单词数
LineCounter
方法:countLine
功能:计算非空行
输入:已经过预处理的字符串
返回:字符串有效行数
FrequencySorter
方法:sortFrequency
功能:计算单词频度,并排行前十
输入:已填入频度计数的HashMap
返回:频度排行前十单词的HashMap的ArrayList
StringAnalyser
方法:countLine
功能:切分单词,统计有效单词及其频度至一个HashMap
输入:已经过预处理的字符串
返回:已填入频度计数的HashMap
FileReader (性能改进后加入)
方法:readFile
功能:读取文件文本,预处理为字符串
输入:输入文件名
返回:已经过预处理的字符串
FilePrinter
方法:writeFile
功能:输出统计结果
输入:字符串总字符数,总单词数,有效行数,频度排行前十单词的HashMap的ArrayList,输出文件名
输出:打印以上数值至文件
WordCount(性能改进前)
private static String inputFileName;
private static String outputFileName;
。。。
//其他变量声明以及初始化
//构造函数
。。。
public void Count()
{
//从文件读取字符串
//调用各类中的方法进行统计
}
public void Print()
{
//输出结果至文件
}
public static void main(String[] args)
{
WordCount cmd;
。。。//参数输入异常判断
cmd = new WordCount(args[0], args[1]);// 传入参数(输入输出文件名)
cmd.Count();
cmd.Print();
}
实现过程(与部分性能改进)
读取文件
使用BufferedReader读取,StringBuilder拼接。
最一开始采用的是if(line = bufferedReader.readLine()) != null)的判断,然后逐行拼接,并在末尾加上\n,顺便还能计算非空行数,算是快的方法了。但经过测试,这样在文本量大特别是行数多时,append的过程很耗时,且助教与同学们的QA中提到的\r判断会使统计结果出现偏差,再在其基础上为各种特殊情况做判断的话,我自己脑袋转不过来,觉得有点麻烦,故不再使用。
。。。//创建BufferedReader
StringBuilder contents = new StringBuilder();
Map<String, Long> words;
int in;
while ((in = bufferedReader.read()) != -1)
{
contents.append((char) in);
}
content = contents.toString().toLowerCase();
words = StringAnalyser.analyseString(content);
//处理完毕,开始统计
charCnt = CharCounter.countChar(content);
。。。
字符串处理
拆分并获取Map,先通过StreamApi的split(正则表达式)拆分字符串,然后通过filter,采用自动机分析过滤掉不符合格式要求的单词,最后通过collect收集并计数单词,获得一个Map,key即为单词,value为出现次数。
正则表达式[^0-9A-Za-z]意为除字母数字以外的任何字符,因为文本预处理中已经将字符全转化为小写,这里的A-Z可以去掉。
比较复杂的自动机实例见:表示数值的字符串(自动机状态转移)
private static String INVALID_WORD_REGEX = "[^0-9A-Za-z]";
public static Map<String, Long> analyseString(String content)
{
List<String> list = Arrays.asList(content);
Map<String, Long> words = list.stream().flatMap(w -> Stream.of(w.split(INVALID_WORD_REGEX))).filter(w ->
{
int i = 0;
char[] chars = w.toCharArray();
if (w.length() >= 4)
{
for (; i < 4; i++)
{
if (!Character.isLetter(chars[i]))
{
return false;
}
}
}
else
{
return false;
}
return true;
}).collect(Collectors.groupingBy(w -> w, Collectors.counting()));
return words;
}
统计
统计总字符数和单词数就是返回字符串的length和map的size这里就不贴代码了。
统计非空行,使用正则表达式直接去匹配,维护一个临时变量用于计数并返回,代码比上文提到的在读取文本和拆分过程中统计更清晰简单,但对比其他同学的单元测试结果,这样做应该是牺牲了性能,因为文本量大时通过正则表达式去匹配即find()很耗时。
private static final String VALID_LINE_REGEX = "(^|\n)\\s*\\S+";
private static final Pattern VALID_LINE_PATTERN = Pattern.compile(VALID_LINE_REGEX);
public static int countLine(String content)
{
。。。//变量初始化
matcher = VALID_LINE_PATTERN.matcher(content);
while (matcher.find())
{
//利用正则表达式匹配有效行,空行不统计,包括\t \r \n 空格以及由他们组成的情况
cnt++;
}
return cnt;
}
统计频度前十单词,将字符串处理获得的map再通过StreamApi进行排序处理,过程较简单,描述见注释。(参考博文:使用Java8 Stream API对Map按键或值进行排序)
public static final int MAX_SIZE = 10;
public static ArrayList<HashMap.Entry<String, Integer>> sortFrequency(HashMap<String, Integer> words)
{
words = words.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue()// 值升序排序
.reversed() // 倒序为降序
.thenComparing(Map.Entry.comparingByKey()))// 键排序(字典序)
.limit(MAX_SIZE) // 限定为前MAX_SIZE个
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(oldVal, newVal) -> oldVal, LinkedHashMap::new));// 返回map
。。。//转换为ArrayList:freqList
return freqList;
}
输出
打印至指定文件,使用BufferedWriter比直接用FileWriter高效点
bufferedWriter.write("characters: " + charCnt + "\n");
。。。//单词数和行数
for (HashMap.Entry<String, Integer> map : freqList)
{
bufferedWriter.write(map.getKey() + ": " + map.getValue() + "\n");// 打印HashMap的键与对应值
}
六、性能改进
在未做任何性能改进,即上文提到的,使用readLine读取,甚至没用StringBuilder而是BufferWriter直接拼接换行符时,仅统计200w个单词(约2900w字符,夹带不规则无效字符)就已经需要约12秒的时间(12000ms+,忘记截图了)。其中读取文件和字符串处理明显占了大头,特别是文本量巨大时的字符串读取、拼接和拆分操作,因此做出如下改进:
初步改进思路
1、将trim(),replaceAll()替换分隔符,循环中append()等一些非必要、可替代、多余的字符串处理删去,改用更高效的正则表达式来进行统计,在统计较大文本量时每改一步都能观察到性能提升。
2、尽量重用对象
特别是String对象的使用,出现字符串连接时使用StringBuilder/StringBuffer代替BufferedWriter。由于Java虚拟机不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理,因此,生成过多的对象将会给程序的性能带来很大的影响。
3、尽可能使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。代码中体现在计数变量cnt等。
4、及时关闭流
进行I/O流操作时,要小心谨慎,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销。还有对应的异常处理也要注意,防止资源泄露。
5、使用带缓冲的输入输出流进行IO操作。带缓冲的输入输出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,这可以极大地提升IO效率。这在实现过程中都已有采用,最终代码在输出结果的方法中有采用,
6、尽量采用懒加载的策略,即在需要的时候才创建。但在文本量大时,这种改动提升不明显,且对一些代码美观性可读性有影响,似乎可以按个人习惯来。
根据以上方法论重审代码并做出改进后,统计200w单词的耗时稳定在7.5s至8s,提升了约1/3,但还是算很慢。
IO性能改进
1、阅读了最先提交作业的几位同学的博文,发现可以通过MappedByteBuffer读取文件。
(参考博文:深入浅出MappedByteBuffer)
采用该方法后,统计百万级别的单词数用时大大缩短,综合其他改进后,统计规则字符、单词可以进入1秒内,可以尝试千万级别了。
public static String readFile(String inputFileName)
{
。。。//数据初始化
try
{
mappedByteBuffer = new RandomAccessFile(file, "r")
.getChannel()
.map(FileChannel.MapMode.READ_ONLY, 0, len);
// 通过RandomAccessFile获取FileChannel,并通过FileChannel.map方法,把文件映射到虚拟内存,返回逻辑地址。
if (mappedByteBuffer != null)
{
return StandardCharsets.UTF_8.decode(mappedByteBuffer)
.toString().toLowerCase(); // 转换为全小写的字符串
}
else
。。。// 空白文件则返回空
}
。。。//异常处理与垃圾回收
return "";// 未读取到则返回空
}
}
2、经过搜寻各种“Java性能优化”的经验博文,发现可以使用StringTokenizer搭配正则表达式,逐个切分、匹配、计入有效单词至HashMap,来代替原来的StreamApi的spilt()-自动机方法。
其中正则表达式[a-z]{4}[0-9a-z]*意为前四位为字母,后不跟或跟任意数量数字/字母(全小写)
采用该方法处理字符串,综合其他改进后,统计1000w规则单词(9000w字符)从约11000ms提升至约6800ms,性能较为可观了。
public class StringAnalyser
{
private static final String FILTER_REGEX = "[^0-9A-Za-z]";
private static final String VALID_WORD_REGEX = "[a-z]{4}[0-9a-z]*";
public static HashMap<String, Integer> analyseString(String content)
{
int cnt; // 临时变量用于计入键值(单词出现次数)
String word;
HashMap<String, Integer> words = new HashMap<String, Integer>();
StringTokenizer tokenizer = new StringTokenizer(content.replaceAll(FILTER_REGEX, " "));
// 先将分隔符全替换为空格,再利用 StringTokenizer 切分单词
while (tokenizer.hasMoreTokens())
{
word = tokenizer.nextToken(" ");
if (Pattern.matches(VALID_WORD_REGEX, word))
{
// 利用正则表达式统计有效字符:至少有四位且都为字母,后跟若干字母或数字,不区分大小写,计入HashMap。
if (words.containsKey(word))
{
// 单词已统计到过
cnt = words.get(word);
words.put(word, cnt + 1);
}
else
{
// 单词初次统计到
words.put(word, 1);
}
}
}
return words;
}}
采用线程池
最终WordCount.java中的主要代码(Count()合并了Print()方法):
public void Count()
{
final String content = FileReader.readFile(inputFileName);// 从文件读取字符串
final HashMap<String, Integer> words = StringAnalyser.analyseString(content);// 从字符串拆分有效单词,统计入HashMap
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> charCnt = executor.submit(new Callable<Integer>()
{
// 统计字符数
public Integer call()
{
return CharAndWordCounter.countChar(content);
}
});
。。。//其他统计
try
{
FilePrinter.writeFile(charCnt.get(), wordCnt.get(), lineCnt.get(), freqList.get(), outputFileName);// 打印结果
executor.shutdown();
}
。。。//异常处理
。。。//main函数没有改动
}
采用多线程后,百万及以下的单词量的性能可以获得约12%~25%的显著提升,文本量越大提升越不明显,最终,统计1000w规则单词(5000w字符),最快可以达到4900ms左右;9000w字符时耗时6000ms左右。而最开始提到的统计200w个单词(约2900w不规则字符),可以达到4500ms左右,提升62.5%。部分测试及截图见单元测试展示模块。
七、单元测试展示
代码覆盖率
如下图,总覆盖率达到78.7%。原本的覆盖率浮动在只有50%~60%左右,查看签入记录发现,在代码不断优化的过程中,通过
- 减少不必要的if判断
- 删除不可达代码
- 用更妥善的方法编写异常处理
- 合并部分简单的类
- 传参优化、简化逻辑、消除重复代码
等方法可以使覆盖率得到提升。
单元测试
性能测试
构造思路:循环拼接字符串并写入文本,调用WordCount.Count进行统计,计算并输出用时。
代码样例如下:
@Test
public void test0() throws IOException
{
final String testString = "software railgun";
。。。//数据初始化,创建writer、builder...
for (int i = 0; i < loopCnt; i++)
{
stringBuilder.append(testString);
stringBuilder.append("\n");
}
writer.write(stringBuilder.toString());
writer.close();
long time = System.currentTimeMillis();
cmd.Count();
System.out.println("time:" + (System.currentTimeMillis() - time) + "ms");
}
结果:
- testString = "software railgun" ;loopCnt = 5000;输出time:120ms
- testString = "software railgun" ;loopCnt = 50000;输出time:251ms
- testString = "software railgun" ;loopCnt = 500000;输出time:862ms
- testString = "software railgun" ;loopCnt = 5000000;输出time:5967ms
- testString = "aaaa bbbb cccc dddd eeee" ;loopCnt = 2000000;输出time:4933ms
- testString = "railgun0 railgun1 。。。(省略2~8) railgun9" ;loopCnt = 1000000;
输出time:5989ms
输出结果截图例
正确性测试
构造思路:总体函数编写同性能测试,只是最后的耗时计算改为
int cnt = 0;//临时计数变量,然后通过调用WordCount去统计,获取值
。。。//创建StringBuilder等
String result = " 。。。";//或预设预想结果(输出到文件的字符串)
Assert.assertEquals(cnt,。。。);
Assert.assertEquals(str.toString(),result); //最后作对比
- 空白文件测试。预计结果:
String result = "characters: 0\nwords: 0\nlines: 0\n";
- 字符数统计测试。输入:
"qwer!tyui[opas]dfgh\r\n|jklz\txcvb(nm"
预计结果:Assert.assertEquals(charCnt,34);
- 综合测试。输入:
"abcdefg\r\nhijklm n\r\n\t\r\n\topqrstuvwxyz\r\nfile123;123file;file;FILE;File\r\n"
预计结果String result = "characters: 71\nwords: 5\nlines: 4\nfile: 3\nabcdefg: 1\nfile123: 1\nhijklm: 1\nopqrstuvwxyz: 1\n";
- 频度统计字典序测试 输入:三组aaaa95,aaaa98和aaaa2000,两组aaaa0~aaaa7,一行一个
预计结果String result = "characters: 167\nwords: 11\nlines: 25\naaaa2000: 3\naaaa95: 3\naaaa98: 3\naaaa0: 2\naaaa1: 2\naaaa2: 2\naaaa3: 2\naaaa4: 2\naaaa5: 2\naaaa6: 2\n";
结果图,均测试通过
八、异常处理
- IO异常,包括FileNotFoundException与其他IOException
- 线程异常,InterruptedException:如果interrupt在计算完成之前在等待的线程上调用,则会抛出。ExecutionException:如果涉及的计算抛出异常本身,将会抛出。
- 因为本次作业逻辑较为简单,无其他特别的异常设计。
九、心路历程与收获
心路历程
- 一开始看到题目的基本要求(正确性方面),感觉并不难,但是越仔细阅读其他要求越觉得复杂,要注意的细节非常之多,还有无数隐藏的问题做的时候可能才会发现,顿感压力倍增。以往的课程里我只是学习了打代码本身,基本上对于“软件工程”中的“工程”过程没有一点的学习与实践,坦白了说就是职业素养培养的很少。所以这次作业就是要以“工程”的思路与标准要求来实践,再简单的算法或代码问题也变得要认真严谨对待了。
- 在2月28号已经实现了题目的正确性要求,但代码跑得很慢,为了优化性能,查了很多资料,也参考了同学的博客,尝试了各种各样的方法,逻辑结构几经修改,代码几经重构,这个过程非常的漫长,深感最开始需求分析、设计中选取性能良好且编写方便的代码组织逻辑、数据处理方法的重要性。虽然因我编程基础一般,最终优化出的性能还是一般,但相比一开始还是有提升的,感受到了不足,但也有一定的成就感。
- 单元测试虽方便,但编写代码时候接口的设计还是会很头疼,这又涉及到最开始的需求分析、设计上了,即一开始决定数据结构、参数如何传递时就要考虑好之后的扩展性,或直接从编写单元测试开始。不然一直返工、改来改去很痛苦。
- 严谨是一件非常重要的事情,即使是字符统计中的\r\n,有效行统计中的空行判断等小细节也是几经讨论,尝试了好几种处理方法才得到了最终的代码。
- 最后,多线程还是不是很熟悉,作业里的编写是参考了网络上的实例,对其本质并不是很理解,用的也不是很好,要继续加深学习。
收获
- 学会使用代码管理工具github(destop),如何fork、commit、pull。总计进行了20多次签入,真的很方便。
- 学会如何编写单元测试,使用的是eclipse的JUnit,无论是测试正确性还是性能都很方便,再也不是只会麻烦地做手动测试了。
- 思考并学习了优化代码性能的各种方法,学习使用了一些Java8的新特性
- 制定统一代码规范并遵循,学习了eclipse如何个性化设置代码风格,并一键格式化代码,培养良好的代码风格。