寒假作业2/2

这个作业属于哪个课程 2021春软件工程实践|W班 (福州大学)
这个作业要求在哪里 寒假作业2/2
这个作业的目标 1.阅读《构建之法》并提问
2.完成词频统计个人作业
其他参考文献 简书、CSDN

part1:阅读《构建之法》并提问

1.在双方默契不足或根本不了解对方的前提下,结对编程的效率和质量真的高于独立编程吗?

    4.5.2的讲到了“在结对编程中,因为有随时的复审和交流,程序各方面的效率和质量取决于一对程序员中各方面水平较高的那一位。这样,程序中的错误就会少得多,程序的初始质量会高很多,这样会省下很多以后修改、测试的时间。”书中对合作能产生更大效能赋予了很多积极的描述,但是却忽视了一个先决条件,在两个人对彼此擅长的部分不甚了解的前提下,强行结对编程往往适得其反。
    我有切身感受。学数据库这门课的时候要学生写一个考试系统,组内有两个同学完成教师端的编程,一人编写一人审查。结果很久都没有拿出测试好的代码,因为审查的同学工作还未开展。结对编程麻烦的一点是二者有一人拖延,整个项目工期都会延长。正是我的个人经验告诉我结对编程之前,需要对伙伴有充分了解,才能确定合作关系。
    再有,结对编程是需要时间磨合的,编程习惯和思维都需要磨合,等磨合得差不多了时间也浪费了一截。如果只是短期合作,效率和质量很难比得上单人开发。因为思维方式差异,结对编程不如单人编程思维来的连贯,自己写的代码自己最了解;另一方面,有的人合作出力不多,水平不高还喜欢指挥,或者不断提问题,遇到这样的伙伴,原本高水平的程序员可以一人完成的项目,在他人的影响之下难以施展本领。还有一种情况,两个人擅长的部分都是前端或都是后端,谁来做编写完整的系统呢?再开始现学吗?谁来做这块没有涉及过的领域呢?时间会浪费在争论与拖延上,因为人的天性都是不希望走出自己的舒适圈的。
    我觉得结对编程的优势是有适用条件的,要么两个人有默契,要么在技术方面互补,不然结对编程质量常常会低于独立编程。所以我还是对作者的观点抱有部分质疑。

2.PSP应该支持软件工程师的个性化定制,还是说专家的建议就是最佳的?

    2.3提及了PSP表格。PSP表格相当于把软件工程师的步骤给写死了,文中提到的说法也大都诸如此类:根据最新的版本(PSP2.1)来看看一个软件工程师在接到一个任务之后应该怎么做。书中完全没提到软件工程师在开发流程中的灵活性和主动性。不光是我,很多程序员看完会有疑问:步骤的粒度是否可以更大或更小?其他形式的计划会不会更加有效?
    我在每次开始编码前会画一个流程图,至少我可以知道开发步骤的递进顺序,回头再审核一下每个步骤是否达到预期要求了,这是我的经验。PSP给人一种很难从哪个步骤开始的感觉,因为不够直观。还有步骤是交叉在一起的,应该用并行的图示展现出来,甚至大多数时候开发都多任务并行的,PSP看起来就像做完一个任务才能开启下一个任务。
    我查过资料,PSP的官方解释是:PSP是一种可用于控制、管理和改进个人工作方式的自我持续改进过程。也就是说PSP的作用重在改进。但奇怪的是怎么预估每一步的时间需求,首先起点就是错误的吧。现实情况往往和理想相背离,哪怕是用户突如其来的需求都会打乱整个流程。即使整个项目完成后,我们更关注的不应该是理想与现实的时间差异,因为估计本来就是不准确的。我个人感觉,需要记录的是进程中各个步骤的哪些地方做了无用功,下次应该规避,这样才有改进成效。

3.IT寡头们真的会因为拥有了成熟的市场和成熟的技术,就会停下创新的脚步?

    16.1.7说成功的团队未必更能创新。原文是这样说的:“当一个团队拥有成熟的市场、成熟的技术、稳定的客户时,团队已经知道用户想要什么,也不想引入太多变数,只要保证这些用户继续使用产品,并继续升级就好了”。
    我不太同意这样的话,应该说大公司有更多的机会走创新之路。我的观点是这样的:十多年前BAT还没有今天这么强大,字节跳动、美团、滴滴都还未成立,现今的阿里已经庞大到将饿了么全额收购。创新热潮是有时间点的,近年来个人创新的消息并不如大公司多,究其根本是创新人员都在积极投身成熟的团队。书中这句话放在早年比较合适,曾经的中国IT也处于幼年期,现在已经是青壮年了。
    从网上资料可以看到,诸如达摩院的研究院、腾讯安全联合实验室,是很难靠一些个体组织起来的,内部都是顶级的科学家或优秀的程序员。从一些数据可以得出当下的创新,个人的创新相比已经有基础的团体并不会有更大的优势。
    此外,大型的科技公司并不会安于现状,停止创新。已经很强大的团队即使创新思维枯竭了,也会有新鲜血液流入,资本会吸引很多创新者,足够庞大的资本是维持创新的关键因素。过去的公司在初期创办的时候是最危险的,他只能靠手中握有的创新牌和大公司博弈。现在人才有很多选择,即使没有运营资金,只要点子足够吸引人,就可以加盟大厂,虽然风险降低了,但同时也失去了获得更大利益的可能。

4.在团队中,每个人的水平层次不尽相同,会演变成平等对话的开发模式?还是说有等级差异的模式更符合现实逻辑?

    这个问题是我看完5.2.9的功能团队模式得到的。原文是这么说的:“很多软件公司的团队最后都演变成功能团队,简而言之,就是具备不同能力的同事们平等协作,共同完成一个功能。在这个功能完成之后,这些人又重新组织,和别的角色一起去完成下一个功能。他们之间没有管理和被管理的关系”。按照作者的说法,功能团队在能力参差不齐的情况下站在平等的角度。我很难理解这话的含义,在我看来,当人与人有差距时,很容易发展成5.2.10的官僚模式。不管是现实生活感受到的,还是道听途说来的,没有哪个公司的团队采用的是平等对话的方式,只有水平相当的人才会在同级职位说着平等的话。
    大二的时候去网龙参观,获悉网龙和阿里一样有着P(技术岗)和M(管理岗)从上至下的分级,即使我认识的人,他们开着十几个人的小公司,一样会有两个管理人员。稍微有能力一点的人说话更有分量,水平差一点的自然会听从安排,其实这样对整个团队的开发才有利,效益才容易最大化。当然,诸如官僚模式未必就是所有团队采用的模式,像交响乐团模式也是很常见的,模式各有千秋。
    每个人都会渴望领导工作团队,官僚模式的好处在于引入了竞争和优胜劣汰的准则。吃大锅饭肯定会使人有惰性,每个人都会想,没有人管理我,我可以少干点活或挑喜欢的活干。5.2.9还有提到一句话,“每个小组都是一个有自主权的单元,可以自由选用最有利于他们完成工作的任何技术”,这听起来就不现实,顶多出现在学生实践项目。与其固守一种模式,追求有名无实的平等,不如换一种思路尝试一下。

5.软件工程和计算机科学学的东西很相似,本科可否将二者合并为计算机软件与理论?

    1.2.1讲软件工程与计算机科学的关系,其中有段话,“很多同学在报名时不知道它们的区别,进去之后发现除了收费高低不同,学的科目差不多,毕业后大部分同学都是写程序,似乎差别不大?其实,它们的区别还是挺大的”。
    作者认为差别挺大,但这个差别大与不大得看从哪个角度看。按照学科性质,计算机科学是研究与实践并重的学科,对计算机未来发展有导向作用;软件工程侧重实践且与现实需求联系更加紧密。按照大学教学来看,我也翻了下计算机专业的修读指南,二者的课程大致相同,除了一些选修课。
    我有一些个人见解。其实本科在计算机科学(或者干脆叫计算机软件与理论)底下分几个方向,其中一个方向是软件工程,不同方向的人专攻一个方面,同时还可以共享选修课。软件工程同样少不了机器学习,javaee也不是就和计算机科学无关,可共享的部分抽取出来供感兴趣的学生选择,同时又保留每个方向的专业性,分配会更加合理。
    然而计算机科学与软件工程的培养目标又有差异,培养出的学生在每个方面的能力也不同,尤其是软件工程的实践性强于计算机科学。所以我的问题包含的成分多是疑惑,希望有朋友能给出更行之有效的答案。

part2:WordCount编程

1.Github项目地址

PersonalProject-Java

2.冷知识和故事

    1975年,艾伦和盖茨给Altair 8800计算机写了个BASIC解释器卖给MITS,他们很快完成了解释器,甚至包括自己的IO系统和编辑器,一共只需要4k内存。 不过最后他们发现还需要一个引导程序将这些东西从外存整进去。 Paul Allen在飞机航班上完成了这项工作。这是1975年,没有笔记本。他用的是纸笔。写的是8080机器码。

3.PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
• Estimate • 估计这个任务需要多少时间 1590 1670
Development 开发
• Analysis • 需求分析 (包括学习新技术) 180 160
• Design Spec • 生成设计文档 80 70
• Design Review • 设计复审 20 30
• Coding Standard • 代码规范 (为目前的开发制定合适的规范) 90 90
• Design • 具体设计 90 100
• Coding • 具体编码 450 500
• Code Review • 代码复审 120 100
• Test • 测试(自我测试,修改代码,提交修改) 200 400
Reporting 报告
• Test Repor • 测试报告 210 230
• Size Measurement • 计算工作量 40 30
• Postmortem & Process Improvement Plan • 事后总结, 并提出过程改进计划 100 120
合计 1590 1679

4.解题思路描述

任务可以拆解成几个部分:
    用字符流读取输入文件
    统计字符数
    统计单词数
    统计有效行数
    统计频率最高的10个单词的出现次数
    用字符流将统计结果输出到文件
依照需求,需要考虑几个方面:类设计,读取方式,数据结构模型,I/O流类的选择。

(1)类设计

    这次任务不需要很多复杂模块拼接,所以我仅划分出两个类:Lib,Word。对字符、单词的判断和统计,还有有效行数的统计,都作为Lib类的方法,封装在Lib中;Word类的作用是让单词和出现次数紧密结合,尤其是在排序的时候,可以随次数递减,随字典序递增。(Word类结构如下,Lib类结构见“设计与实现过程”的思维导图)

class Word implements Comparable<Word>{

        String word;     //单词
        int frequency;    //单词出现次数

        Word(String s,int frqy){
            word = s;
            frequency = frqy;
        }

        @Override
        public int compareTo(Word w) {
            if(this.frequency > w.frequency)
                return -1;
            else if(this.frequency < w.frequency)
                return 1;
            else
                return this.word.compareTo(w.word);
        }
    }

(2)读取方式

    最开始有想过将文件一整行读出,当作一个字符串,或将整个文件的所有字符拼接成一个字符串。然后再对字符串用正则表达式。但是分行读取会把单词割裂,而把所有字符拼接成一个字符串开销又是相当大的,需要大量申请和释放资源。
    后来我想能否一边读取单字符同时一边处理单词。经过设计发现是可行的,读取过程中可以将字符拼接直至遇到分隔符,再对已经积累成单词的字符串作判断。到这读取方式就确定了。

(3)数据结构模型

    如果用了上述读取方式,我认为单单使用TreeMap很难完成对出现次数排序的任务。
    于是我上网百度了各种数据结构。TreeMap的红黑树结构查找效率不算高,所以查找是可以优化的。HashMap在搜索查找这一块体现出良好的性质,TreeSet能同时排序出现次数和单词字典序(如果次数作为键值,TreeMap只能排序次数),所以综合以上,采用HashMap+TreeSet的数据结构。

(4)I/O流类的选择

    操作系统学过,I/O需要从外设读取写入,涉及机械运动,是程序性能的瓶颈段。如果有缓冲机制帮助程序读取文件中的内容,整个流水线的效率会大大提高。在多个字符流类中,BufferedReader与BufferedWriter设置了缓冲区,这两个类适合用于I/O。

5.代码规范制定链接

codestyle.md

6.设计与实现过程

    src下有2个java文件,一个是包含主函数的WordCount.java,另一个是Lib.java。Lib.java下有两个类:Lib以及Lib的内部类Word。Lib.java内部的思维导图如下:




    以下是程序流程图:

(1)I/O

    I/O这块采用的是BufferedReader与BufferedWriter。I/O是一个很大的性能瓶颈,减少I/O次数就是提高效率。BufferedReader和BufferedWriter在内存中会自带一个8kb的字节缓冲区,可以从缓冲区读取或写入。

(2)统计字符和单词数

    函数countCharAndWord的功能包括了统计字符和单词数。之所以没有将两个功能拆分到两个函数,是因为整个文件的所有字符扫描两遍,分别统计字符数和单词数,时间消耗大约会是扫描一遍的两倍。所以将两个功能放到一个方法里面,组成countCharAndWord函数。
    countCharAndWord函数代码:

public void countCharAndWord() throws IOException{
        int c;
        String curWord = "";
        while ((c = reader.read()) != -1){
            //ASCⅡ码在0到127之间的属于字符(汉字不包括在内)
            if(c >= 0 && c<= 127) {
                CharNum++;
                //如果c为数字或字母,则作为单词的一部分
                if (Character.isLetterOrDigit(c)) {
                    curWord += (char) c;
                }
                //如果c为非数字或字母的字符,则作为分隔符
                else {
                    if (isWord(curWord)) {
                        WordNum++;
                        wordToHashMap(curWord);
                    }
                    curWord = "";
                }
            }
            //如果c为中文,则作为分隔符
            else{
                if (isWord(curWord)) {
                    WordNum++;
                    wordToHashMap(curWord);
                }
                curWord = "";
            }
        }
        //对最后一个单词进行处理
        if(isWord(curWord)){
            WordNum++;
            wordToHashMap(curWord);
        }
        writer.write("characters: " + CharNum + "\n");
        writer.write("words: " + WordNum + "\n");
}

    countCharAndWord函数的流程图如下:

    流程图里的“单词插入HashMap”实际上是调用了函数WordToHashMap。这个函数的作用是查看key值是否已经存在于HashMap,若已经插入,则value值+1;若还未插入,则将(key,value)=(单词,1)插入HashMap。

(3)统计有效行数

    函数countRow功能即为统计有效行数。按行读取字符串(readLine),对字符串用trim()函数处理,处理后如果为空,则原字符串为空字符串。
    countRow函数代码:

public void countRowNum() throws IOException{
        String row;
        BufferedReader reader1 = new BufferedReader(new FileReader(inputFile));
        while((row = reader1.readLine()) != null){
            //只统计包含非空白字符的行
            if(!row.trim().isEmpty())
                RowNum++;
        }
        writer.write("lines:" + RowNum + "\n");
}

(4)输出频率最高的10个单词

    函数printFrequency用于输出频率最高的10个单词。先前函数countCharAndWord已经将所有单词插入HashMap,现在将所有单词从HashMap取出,打包成Word类对象,并插入TreeSet。经过TreeSet排序后得到频率前十的单词。看起来好像有点繁琐,但实际上这样做在查找和排序时比其他方法有更高的效率:

(i)查找算法

    在对单词的出现次数进行修改时,需要查找这个单词。最合适的是采用单词为key,出现次数为value这样的数据结构,所以我使用HashMap。HashMap能提高查找性能,并且相较于TreeMap在效率上具有优势:HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序。HashMap的搜索时间基本控制在O(1),而TreeMap的搜索时间为O(log(n))。
    以下是查找算法主要代码:

public void wordToHashMap(String curWord){
        String lowerWord = curWord.toLowerCase();
        Integer times = map.get(lowerWord);
        //times==null说明这个单词已经插入hashmap
        if(times != null)
            map.put(lowerWord,times+1);
        else
            map.put(lowerWord,1);
}
(ii)排序算法

    使用TreeSet的目的是缩短排序时间。TreeSet具有的排序功能是HashMap所不具备的,HashMap肯定不能作为排序工具。TreeSet的底层实现虽然和TreeMap一样,而且排序性能没区别,但是由于作业要求同次数的单词按字典序输出,TreeMap没法实现,因为TreeMap只能按单一键值排序。所以最后选择了TreeSet,将单词和出现次数打包成Word类,并且让Word类重写了Comparable接口。
    以下是排序算法主要代码:

        Iterator<Word> iterator2 = set.iterator();
        int i = 0;
        while (iterator2.hasNext()) {
            Word w = iterator2.next();
            writer.write(w.word + ": " + w.frequency + "\n");
            i++;
            if(i >= 10)
                break;
        }

7.性能改进

(1)I/O性能

    由于FileReader和FileWriter每次读取都要通过I/O操作,效率较低,所以考虑是否有办法一次I/O,多次利用。BufferedReader和BufferedWriter就具有缓冲机制,明显减少了频繁的I/O操作所产生的时间消耗。选择BufferedReader和BufferedWriter对性能的提升有显著效果。
BufferedReader读取200w个字符的代码与所需时间:

FileReaderReader读取200w个字符的代码与所需时间:

(2)统计性能

    字符统计和单词统计可以同时进行,仅需一遍读取文件操作。遇到分隔符就对字符串进行一次判断,满足条件者+1。一遍读取比分批读取时间消耗减半。

字符和单词并行耗时:

字符和单词分别统计耗时:

(3)查找与排序性能

    比起单一的TreeMap,HashMap+TreeSet有较高的性能。HashMap借助散列技术加快查找,TreeSet在排序时比TreeMap更加灵活,本次作业将二者组合使用。

(4)程序整体性能

1w个单词,68786个字符:

用时41ms

100w个单词,6864630个字符:

用时406ms

1000w个单词,68673272个字符:

用时3310ms

1000w单词运行结果:

8.单元测试

(1)统计字符数与单词数测试:

public void countCharAndWord() throws IOException{
    lib.countCharAndWord();
    assertEquals(lib.getCharNum(), 68662400);
    assertEquals(lib.getWordNum(),10000000);
}

    测试数据是字符串数组中获得的,每种字符串选取固定个数,总单词数1000w。将程序统计结果与理想值对比即可。

(2)统计有效行数测试:

public void countRowNum() throws IOException{
    lib.countRowNum();
    assertEquals(lib.getRowNum(),666667);
}

    在构造测试数据时,每输出15个单词输出一个换行符,共有1000w/15=666667个换行符。经比较,程序统计与期望值相符。

(3)单词出现次数top10测试:

public void printFrequency() throws IOException{
    lib.printFrequency();
    Iterator<Lib.Word> iterator = lib.set2.iterator();
    int i = 0;
    while(iterator.hasNext()){
        Lib.Word w = iterator.next();
        assertEquals(w.word,s[i]);
        i++;
    }
}

    构造数据时,每个单词的出现次数已经用循环固定了,所以top10很容易知道。同样,将统计结果和实际排序比对。

(4)单元测试与覆盖率截图



(5)优化覆盖率的方法

    将方法分割细化可以提高覆盖率;尽量避免使用三目运算符,多if条件判断;设置合理测试数据,考虑多种情况;尽可能使每一个if分支,switch的每一个case的内容都得到执行。

9.异常处理说明

    调用文件流相关函数时会出现I/O异常,例如BufferedReader.read(),BufferedReader.read().close()。Lib类方法的异常抛出到main函数,main函数的异常在函数内处理。处理方法是在主函数内用catch捕获,在命令行打印异常信息:
文件不存在时:

参数过多或过少会同样设置了异常,例如未键入参数:

只有一个参数:

参数大于两个:

    文件不存在异常的目的告诉用户文件不在指定位置,需要手动搬到指定位置或改变路径;参数过多或过少异常目的是提醒用户输入的参数个数错误。

10.心路历程与收获

    整体过程还是比较顺利的,确定架构之后就没有改变了。因为第一次构思就反复考虑,所以数据结构在实现的时候没有很大改动。需求很明确,最开始我就一个功能一个函数,到了后面发现多遍读取文件没有必要,把读字符和单词并到一个函数了。作业完成后我用其他方法实现了,相较之下不如最初的方法性能更理想。总的来说,这次任务的设计和实现没有过于困难。PSP对编程也有一定的引导作用,PSP、git都算是编程之外学到的东西了。以下是我的一些收获:

(1)git的使用

    这次作业最大的收获是学会了git的使用,并接触了github平台。我平时没有使用github的习惯,光注重代码本身了,软件工程课程也算是鞭策我学习代码管理工具的使用。代码能力固然重要,工具的熟练运用也能使工作效率有很大的提升。像github这样具有丰富学习资源的网站,今后我会多多加以利用。

(2)学会用博客总结

    通过写完程序以后的总结,给我加强了一个意识,博客可以作为记录一个项目从入手到完善的记事本。从如何思考、搜索资料、设计、实现、性能评估、优点归纳、把握开发流程与时间线这些维度展开深入总结程序可改进、可借鉴之处。这样不仅有利于以后复查,还可供程序员之间交流。借此机会,我完成了从一个博客旁观者到博客撰写者的角色转变。

(3)对java有更深入的了解

    WordCount程序让我对java有了更充分的理解,比如I/O流的性能,过去用的时候没有斟酌过,都是随便用的。现在实操了一下,对buffer机制的在性能上的优越性有了直观感受(运行时间只有连续I/O的一半不到,很nice)。java的数据结构也没怎么用过,只用过C语言的。java的HashMap和TreeMap,如果没查过资料,我都想不起来有什么区别,更别说性能差异和实现原理。数据结构一直做题是很难体会实际应用场景的,一定要结合一些有明确意义的项目练手。

posted @ 2021-03-03 22:30  SakuChyan  阅读(270)  评论(8编辑  收藏  举报
/*以下是点击彩蛋特效*/