寒假作业2/2
这个作业属于哪个课程 | 班级链接 |
---|---|
这个作业要求在哪里 | 作业要求 |
这个作业的目标 | part1:阅读《构建之法》并提问 part2:WordCount编程 |
其他参考文献 | ... |
一. 阅读《构建之法》并提问
1-问题一:好的创新是否常常难以改变旧的长期的习惯?
在P350的迷思三,看到了两个例子,一个是两种键盘,他们的布局不同,第一种更早出现,长此以往便先入为主,但却没有第二种效率来得高,第二种键盘甚至还可以减少劳损。尽管第二种键盘这么好,用的人还是比第一种少了很多。另一个例子是关于衡量制度的选用。如果说是因为前一种制度已经使用很久,深入方方面面,咋一看历经种种困难进行转变,好像并没有那么值得,其原因用文中的:我能从中得到什么?这一问题确实可以解释。但是对于第一个例子,或许对于平常少用键盘的人来说,并无所谓,但是对于那些经常使用键盘的人来说,如果可以尽早的换一种键盘,那熟悉起来并非难事,而且好处明显,并且越早改变可以越早的体会到好处,而且常用键盘的人在现今并不少,为什么使用人数仍是很少?是否说明这是一种习惯下的惰性?如果在有明显的优势之下,并且阻力也不是特别大的情况下,好的创新无法打败旧的习惯,是否会错过一些好的改变,无法形成一些看似更好的习惯?
感觉二者的关系有点像P359-360提到的维持性创新和颠覆性创新的关系。
2-问题二:先发优势和后发优势哪一个更有优势?
在P351迷思四处提到了很多成功的创新者都不是“第一个吃螃蟹🦀的人”,也举了很多后来居上、前浪死在沙滩上的例子。先发可以更早的进入市场,有机会最先享受收益,但是也意味着“摸着石头过河”,存在巨大的风险,而后发者可以先静观其变,之后再其基础上做出改进,反而抢走先发者的市场份额。这是否说明后发优势会更加明显?是否在所有类型的市场都是后发优势更明显呢?要怎么才可以保持自己的先发优势而不被后来者打退甚至居上?如果后发优势如此明显,人人都想做后发者,会出现创新动力降低的情况吗?
3-问题三:如何把握创新的时机?时力与创新能力孰轻孰重?
在P362创新的时机提到了技术采用生命曲线图中的“鸿沟”,太新的东西常常越不过那个“鸿沟”,一个创新的想法在一开始往往激不起什么水花,一段时间之后才被其他的企业看准时机投入市场;又或者是一些小的企业先开始创新但效果一般,之后被大企业看准时机,吸纳小企业后才有较大成效。又如,美国sun电脑公司。该公司有相当长的使用UNIX这一操作系统的历史,并且公司的系统开发工程师在业余时间,也会做一些采用开放源代码技术的项目,但他已经被Linux操作系统抢走了不少市场份额,原因就是它不愿意采用开放源代码的技术。尽管当时它拥有很大的优势去做这件事情,但是没有把握住创新的时机。那么该如何选择创新的时机?掌握和把握住时机的能力叫做时力。时力是否比创新能力对于成功更重要?
4-问题四: 积累对于创新的作用?
P360说到两个例子。Rovio公司在制作了几十个游戏后,又有愤怒的小鸟经历了数千次的修改后才得以问世,取得成功;一个设计公司反复琢磨用户如何使用拖把三年,最后才研发出全新的产品,并创造了很大的价值。这样看来,创新并不是一蹴而就的,需要厚积薄发、厚积厚发。P353还提到一个物理学家在不是他的领域进行创新,竟然发明出了WWW协议。一个是慢慢积累,一个是在别的领域突发奇想,形成了巨大的反差。那么知识的积累是否对创新有较大的帮助?普通人希望培养创新能力是否需要先投入大量的时间成本去学习,还是说在在学习的过程中不断地有意识的进行创新?这样是否会因为基础知识不扎实而无法抓住创新的要点?
5-问题五:实现中遇到突发状况,又存在时间限制该怎么办?
P240从spec到实现,提到在按设计文档实现代码时出现了一些意想不到的问题,如果遇到的问题是比较细节的问题,在设计阶段难以考虑到,可能因为技术问题难以解决,耗时长,耗力大,而阻塞了下一步实现,不幸的是项目的截止时间有限,该怎么办?如果是在团队协作中,自己遇到了问题,无法及时解决而延迟了团队的进度,该怎么处理这样的意外?又该怎么调节心态,不要过分焦虑?
二. WordCount编程
1. Github项目地址
GitHub:https://github.com/Letoo-J/PersonalProject-Java/tree/dev
2. PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
• Estimate | • 估计这个任务需要多少时间 | 1080-1200min | |
Development | 开发 | ||
• Analysis | • 需求分析 (包括学习新技术) | 120min | 150min |
• Design Spec | • 生成设计文档 | 60min | 40min |
• Design Review | • 设计复审 | 60min | 30min |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 60min | 60min |
• Design | • 具体设计 | 60min | 90min |
• Coding | • 具体编码 | 180-240min | 240min |
• Code Review | • 代码复审 | 120min | 50min |
• Test | • 测试(自我测试,修改代码,提交修改) | 60min | 120min |
Reporting | 报告 | ||
• Test Repor | • 测试报告 | 120min | 60min |
• Size Measurement | • 计算工作量 | 60min | 30min |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 120min | 90min |
合计 | ~1140min | 870min |
3. 解题思路描述
解题思路描述。即刚开始拿到题目后,如何思考,如何找资料的过程(3')
- 刚拿到题目时,先列出所有需要了解的知识点,并根据题目的要求和之前同学们的提问和解答整理一下精简的代码实现要求。
- 整理总结代码规范。
- 查找资料:作业要求里的链接和搜索的教程;之后简单复习了一下java文件操作的知识点,和其他一些必要的技术。
- 然后开始思考每一个功能该怎么写,写出伪代码,再选择具体的实现方式。
- 逐个功能编写实现,写完进行单元测试,成功后整理进主函数;
- 进行接口封装
- 进行整体的测试、修改
- 进行性能分析、优化
- 检查代码是否与自己的代码规范匹配
4. 代码规范制定链接
5. 设计与实现过程
计算模块接口的设计与实现过程。 设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(展示出项目关键代码),并解释思路,以及独到之处。(20')
展示出项目关键代码代码时,禁止截图和大段复制;只贴关键性的代码和处理逻辑,并说明清楚。代码展示格式参考。如果不符合要求,该评分项(20')按0分处理。
代码设计:
- 说明:
1.WordUtil
接口中,每种功能的函数有两个重载,参数分别为:fileName
与fileName, fileDirectory
。当只输入文件名时,输入输出文件默认放在此程序文件所在的文件夹目录下;若既有文件名,又有文件夹目录,则将他们拼接即为输入输出文件的绝对路径。
(这样做是为了保证命令行、单元测试、IDE内程序都可以直接正常的运行)
2.IDE运行时,默认输入输出文件名为:input.txt、output.txt
实现过程:
1.countChar:统计文件的字符数
比较简单,逐个字符读取文件即可
File file = new File(fileDirectory + fileName);
BufferedReader bReader;
try{
bReader = new BufferedReader(new FileReader(file));
while((x = bReader.read())!=-1) {
char a=(char)x;
charNum++;
}
bReader.close();
} catch...
2.countWord:统计文件的单词总数
按行读文件,用正则表达式按作业要求分割每行的单词,放入一个String数组。遍历每行分割后的String数组,进行判断并计数
File file = new File(fileDirectory + fileName);
BufferedReader bReader;
try{
bReader = new BufferedReader(new FileReader(file));
String temp = "";
//按行读文件,用正则表达式分割每行的单词
while ((temp = bReader.readLine()) != null) {
String[] words = temp.split("[^a-zA-Z0-9]+");
for (String word : words) {
word.toLowerCase();
//碰到符合条件的单词,单词数+1
if (word.matches("[a-zA-Z]{4}[a-zA-Z0-9]*") ) {
wordNum++;
}
}
}
bReader.close();
} catch...
3.countLine:统计文件的有效行数
先按行读取文件,将读取到的每行存入temp
中,将temp中匹配正则表达式regEx
的字符替换为空字符串,再用trim()
函数去掉字符串两端的多余的空格。若此行全部由空白字符组成,则不计入有效行数。
String regEx = "[\n\\t\\r ]";
int line = 0;
File file = new File(fileDirectory + fileName);
BufferedReader bReader;
try{
bReader = new BufferedReader(new FileReader(file));
//按行读取文件,进行正则处理判断有效行数
while((temp = bReader.readLine()) != null) {
//这里是将特殊字符换为aa字符串," "代表直接去掉
String aa = " ";
Pattern p = Pattern.compile(regEx);
//这里把想要替换的字符串传进来
Matcher m = p.matcher(temp);
String newString = m.replaceAll(aa).trim();
if(!newString.equals("")){
line++;
}
}
bReader.close();
} catch...
4.countWordFrequency:统计文件中各单词的出现次数
与统计文件的单词总数类似,先将每行按分隔符进行分割,并存入String数组。遍历String数组,判断单词正确性,再将单词逐个存入HashMap
中(不区分大小写)。HashMap
的key
为单词本身,value
为其出现的次数。之后用自定义的sort()
函数进行排序,并返回一个List。
- 法一:
List<HashMap.Entry<String, Integer>> wordList = null;
Map<String,Integer> wordMap = new HashMap<String,Integer>();
BufferedReader bReader;
try {
File file = new File(fileDirectory + fileName);
bReader = new BufferedReader(new FileReader(file));
//按行读文件,用正则表达式分割
while ((temp = bReader.readLine()) != null){
String[] words = temp.split("[^a-zA-Z0-9]+");
//在分割出单词之后,将单词遍历储存在hashmap当中,在储存前先判断是否为合法单词
for (String word : words) {
word = word.toLowerCase();
if (word.matches("[a-zA-Z]{4}[a-zA-Z0-9]*") ) {
//若此单词已经记录过,value + 1
if(wordMap.containsKey(word)){
wordMap.put(word, wordMap.get(word)+1);
} else {
//若未记录,初始化value = 1
wordMap.put(word, 1);
}
}
}
}
if(!wordMap.isEmpty()){
//按单词个数排序
wordList = Sort(wordMap);
}
bReader.close();
} catch...
//单词排序函数,将map依次按照出现次数排序,次数相同的按字典序排序
public List<HashMap.Entry<String, Integer>> Sort(Map m){
List<HashMap.Entry<String, Integer>> wordList = new ArrayList<HashMap.Entry<String, Integer>>(m.entrySet());
Comparator<Map.Entry<String, Integer>> com = new Comparator<Map.Entry<String, Integer>>(){
@Override
public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
if(o1.getValue() == o2.getValue()){
//字典序
return o1.getKey().compareTo(o2.getKey());
}
//从大到小
return o2.getValue()-o1.getValue();
}
};
wordList.sort(com);
return wordList;
}
- 法二:即countWordFrequency2,同样先扫描文本得到单词的
HashMap
。自定义一个类WordEntity
(存有单词与其次数等属性,自定义比较函数),Set<WordEntity> wordSet = new TreeSet<WordEntity>();
,之后遍历HashMap
,每个键值对构造一个WordEntity
对象,并放入TreeSet
中,TreeSet
会按照WordEntity
类的比较函数自动排序。【详见具体代码】
6. 性能改进
计算模块接口部分的性能改进。 展示出项目性能测试截图并描述;记录在改进计算模块性能上所花费的时间,描述你改进的思路。(3')
- 概览:
- 改进后:
- 改进后:
- 内存:
- 改进后:
- 改进后:
从上面的图可以发现:byte[]和String使用较多
改进思路:
- 将文本读取到的字符串String尽量用StringBuffer代替;字符串变量用StringBuffer写。String为字符串常量,而StringBuffer为字符串变量。对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以读写速度要比String快很多。
- 使用BufferedReader类读取文本数据:采用BufferedReader类将数据读取到内存,减少IO操作,提高数据读取速度。
- 利用TreeSet进行单词排序:遍历所有单词,并用hashmap存储每个单词及其对应的出现次数,之后遍历hashmap存储每个键值对在wordEntity对象中,再放入TreeSet中。对wordEntity实现comparable接口重写compareTo(),实现TreeSet自动排序。
(耗时:1-2h)
7. 单元测试
计算模块部分单元测试展示。 展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中;如何优化覆盖率?(5')
- 部分测试思路:
将要测试的txt文件放入某个文件夹,之后调用WordUtil的函数只要传入文件夹即可定位输入文件,再将函数返回值与预定值进行比较、直接输出。每个测试函数中可以只测一种或多种函数功能。
public static final WordUtil wu = new WordUtilImpl();
//测试文件目录
public static final String TEMPDIRECTORY = "G:\\SoftEngineeringPractice\\WinterVacation\\ttttttttest\\";
- 部分单元测试代码:
//部分测试函数:
@Test
public void t1() throws IOException {
String fileName = "input1.txt";
assertEquals(112, wu.countChar(fileName,TEMPDIRECTORY));
assertEquals(9, wu.countWord(fileName,TEMPDIRECTORY));
assertEquals(5, wu.countLine(fileName,TEMPDIRECTORY));
String result = "";
List<HashMap.Entry<String, Integer>> wordList = wu.countWordFrequency(fileName,TEMPDIRECTORY);
if(wordList != null){
if(wordList.size() <= 10){
for (HashMap.Entry h : wordList) {
result += h.getKey() + ": " + h.getValue() + "\n";
}
} else {
for (int i=0; i<10; i++){
HashMap.Entry<String,Integer> h = wordList.get(i);
result += h.getKey() + ": " + h.getValue() + "\n";
}
}
} else{
result += "无单词";
}
System.out.println(result);
}
- 部分测试数据及思路:
思路:包含大小写不同的同种单词、各种非单词(数字开头)、各种分隔符、由各种空白符组成的行(无效行)、频数相同的单词若干(用于观察排序)...
- 测试覆盖率截图:
说明:有些未覆盖到的为异常捕捉、处理
- 如何优化覆盖率:
逐个函数测试、尽量找出可能出现的异常、注意每个分支都走一遍
8. 异常处理说明
计算模块部分异常处理说明。 在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。(4')
- 存在的异常类型:
名称 | 说明 |
---|---|
FileNotFoundException | 文件不存在 |
IOException | 文件读写异常 |
- 自定义异常处理:
public void MyException(String fileName, String fileDirectory) throws IOException {
if (fileName == null) {
//System.out.println("未输入文件!");
throw new IOException("未输入文件!");
} else if (fileName.equals("")) {
//System.out.println("文件名为空!");
throw new IOException("文件名为空!");
} else if (fileDirectory == null) {
//System.out.println("文件路径不正确!");
throw new IOException("文件路径不正确!");
} else if (!rightDocument(fileName)) {
//System.out.println("文件格式错误!非txt后缀文件");
throw new IOException("文件格式错误!非txt后缀文件");
}
}
//判断文件名是否为文本文件
public boolean rightDocument(String fileName){
boolean result = false;
if (fileName.matches("[\\s\\S]*.txt") ){
result = true;
}
return result;
}
1.文件名、文件目录不正确
//文件名为null
@Test
public void t003() throws IOException {
String fileName = null;
assertEquals(5, wu.countChar(fileName,TEMPDIRECTORY));
}
//文件名为空
@Test
public void t003() throws IOException {
String fileName = "";
assertEquals(5, wu.countChar(fileName,TEMPDIRECTORY));
}
//文件目录为null
@Test
public void t003() throws IOException {
String fileName = "input";
assertEquals(5, wu.countChar(fileName,null));
}
//文件名格式不对
@Test
public void t003() throws IOException {
String fileName = "input";
assertEquals(5, wu.countChar(fileName,TEMPDIRECTORY));
}
2.文件不存在/找不到
//文件不存在
@Test
public void t2() throws IOException {
String fileName = "inputx.txt";
assertEquals(112, wu.countChar(fileName,TEMPDIRECTORY));
...
}
9. 心路历程与收获
结合在构建之法中学习到的相关内容,撰写解决项目的心路历程与收获。(5')
- 首先是跟着作业安排的PSP表格知道了在动手实践之前的规划很重要。明确需求,找到思路,先设计代码,之后再进行编码,效率会更高,而不是一上来就一通乱敲。这会给人一种明确的目标与进度安排,有条不紊。
- 系统性的完成作业,养成良好的习惯。这次实践不仅仅是要实现功能,还要进行测试,性能优化、异常处理等。这比以往的作业相比,可以更好地锻炼能力,花的时间也多了不少。
- 重新制定了自己的代码规范。之前也弄过这个,但是早就忘到脑后,现在打算重新开始。
- 学习、巩固了许多知识点,了解到尽管之前学习过一些知识,但是没有动手运用就不能化为自己的东西。比如java的文件操作和一些数据结构、正则表达式、git的使用等,尽管之前多多少少接触过,但是真正使用起来还是很生疏。
- 逐步培养规范写代码的能力,对软件工程加深了一点了解。