《构建之法》第四次作业之结对编程

作业要求地址 点一下
GitHub项目地址 点一下
结对伙伴博客 点一下

同样,和上次的程序一样,我们在老师的同意下,采用java+IDEA完成本次作业

零、项目需求

输入文件名以命令行参数传入。例如我们在命令行窗口(cmd)中输入:

wordCount.exe input.txt

则会统计input.txt中的以下几个指标

  • 统计文件的字符数:

  • 只需要统计Ascii码,汉字不需考虑

  • 空格,水平制表符,换行符,均算字符

  • 英文字母:A-Z,a-z

  • 字母数字符号:A-Z,a-z,0-9

  • 分割符:空格,非字母数字符号

  • 例:file123是一个单词,123file不是一个单词。file,File和FILE是同一个单词

  • 输出的单词统一为小写格式

  • 统计文件的单词总数,单词:至少以4个英文字母开头,跟上字母数字符号,单词以分隔符分割,不区分大小写。

  • 统计文件的有效行数:任何包含非空白字符的行,都需要统计。

  • 统计文件中各单词的出现次数,最终只输出频率最高的10个。频率相同的单词,优先输出字典序靠前的单词。

  • 按照字典序输出到文件txt:例如,windows95,windows98和windows2000同时出现时,则先输出windows2000

  • 输出的格式为

·        characters: number
·        words: number
·        lines: number
·        <word1>: number
·        <word2>: number
·        ...
 

一、PSP表格

PSP是卡耐基梅隆大学(CMU)的专家们针对软件工程师所提出的一套模型:Personal Software Process (PSP, 个人开发流程,或称个体软件过程)。

一个功能完备的程序不是一蹴而就的。通过将词频统计的需求划分为4个部分,可将一个大任务划分为可操作的小任务,同时最好按照任务难度或紧急程度指定各个任务的完成次序。因此,在动手开发之前,要先估计将在程序各模块开发所需耗费的时间,以及完成整个项目所需的时间,将这个[估计值]记录下来,写成PSP 的形式。
PSP的目的是:记录工程师如何实现需求的效率,和我们使用项目管理工具(例如微软的Project Professional,或者禅道等)进行项目进度规划类似。

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 15 20
· Estimate · 估计这个任务需要多少时间 300 430
Development 开发 90 110
· Analysis · 需求分析 (包括学习新技术) 30 30
· Design Spec · 生成设计文档 20 20
· Design Review · 设计复审 (和同事审核设计文档) 0 0
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 0 0
· Design · 具体设计 30 30
· Coding · 具体编码 100 120
· Code Review · 代码复审 20 20
· Test · 测试(自我测试,修改代码,提交修改) 30 20
Reporting 报告 30 20
· Test Report · 测试报告 10 15
· Size Measurement · 计算工作量 0 0
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 20 25
合计 395 430

二、结对过程

了解完题目后,简单讨论了项目需求以及项目规范,在明确了需求后,开始使用阿里巴巴开发规范进行开发。程序主要分为统计行数,字母数,单词数,分为三个功能。分配任务后,写出了初代版本,然后调试,修改,进行单元测试。最终形成最终版本。

同时我们命名变量统一使用驼峰命名法。

三、解题思路

看完了整个题目,设计了五个类(不包含内部类)来实现此次需求。

基本的流程便是:访问Main主类通过IO输入流传入文件,首先利用String,StringBuffer类经过简单的处理后,调用我们写好的类利用正则表达式进行统计各种数据,返回即可。详细过程见源代码及其注释:

流程如图:

1570710870282

  • Main类:程序入口主类,输入需要分析的文件路径,调用其他类的方法,最后在程序中生成result.txt文件
package com.zzjAndcyy;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

/**
 * 运行主类
 */
public class Main {

    /**
     * 主要的运行方法,运行该方法,输入文件路径 如F:\test.txt
     * 将会自动生成result.txt结果文件
     * @param args
     */
    public static void main(String[] args) {
        System.out.println("请输入想要统计的文件绝对地址(如:F:\\test.txt):");
        Scanner sc = new Scanner(System.in);
        // 想要读取的文件地址
        String url = sc.nextLine();
        try {
            BufferedReader bufferedReader = new BufferedReader(new FileReader(url));
            StringBuilder stringBuilder = new StringBuilder();
            String line;

            while ((line = bufferedReader.readLine()) != null) {
                // 进行必要的文本处理操作,比如换行符的处理
                stringBuilder.append(line).append("\n");
            }
            // 将文本中的标点符号进行处理
            String content = stringBuilder.toString()
                    .replace(".", " ")
                    .replace(",", " ")
                    .replace("!", " ")
                    .replace("?", " ");

            // 分别创建字母,单词,行数统计工具类,去完成统计
            CharsCount charsCount = new CharsCount();
            WordsCount wordsCount = new WordsCount(content);
            LinesCount linesCount = new LinesCount();

            // 调用相应的方法去得到结果
            // 行数
            int lines = linesCount.linesNumber(url);
            // 字母数
            int characters = charsCount.charsNumber(content);
            // 单词数
            int words = wordsCount.getSum();
            // 前十高频词
            List<Map.Entry<String, Integer>> mostList = new CalMost().mostWords(wordsCount.getMap());
            // 创建输出的结果result.txt文件
            PrintStream printStream = new PrintStream("result.txt");
            System.setOut(printStream);
            System.out.println("characters: " + characters);
            System.out.println("words: " + words);
            System.out.println("lines: " + lines);
            System.out.println("文章中出现的高频词如下:");
            for (Map.Entry<String, Integer> i : mostList) {
                System.out.println("<"+i.getKey()+">: " + i.getValue());
            }

            // 关闭相应的IO流,保证安全
            printStream.close();
            bufferedReader.close();
        } catch (IOException e) {
            System.out.println("文件不存在");
            e.printStackTrace();
        }
    }
}

  • LinesCount 类: 行数统计类,通过使用此类的方法,返回得到文本中的行数
package com.zzjAndcyy;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

/**
 * 统计行数
 */
public class LinesCount {
    /**
     * 根据文件的名称,创建输入流去读取文件的内容
     * 可根据整行读取实现记录读取次数,也就是行数
     * @param filePath
     * @return
     */
    public int linesNumber(String filePath) {

        // 记录行数
        int sum = 0;
        try {
            FileReader fileReader = new FileReader(filePath);
            BufferedReader bufferedReader = new BufferedReader(fileReader);
            String line;
            // 通过循环不断整行读取文件
            // 同时记录读取次数即可
            while ((line = bufferedReader.readLine()) != null) {
                if (line.length() != 0 && !line.matches("\\s+")) {
                    sum++;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sum;
    }
}

  • CharsCount类:字母统计类,通过使用此类的方法,返回得到文本中的字母数
package com.zzjAndcyy;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 提供一个方法,
 */
public class CharsCount {

    /**
     * 根据正则表达式来统计字母数
     * @param content
     * @return
     */
    public int charsNumber(String content) {
        // 编写正则表达式查询规则
        String regex = "\\p{ASCII}";
        Pattern pattern = Pattern.compile(regex);
        // 开始去匹配
        Matcher matcher = pattern.matcher(content);
        // 定义一个int sum 去存储字母数目
        int sum = 0;
        while (matcher.find()) {
            // 找到一个字母,累加
            sum++;
        }
        // 匹配完毕,返回结果
        return sum;
    }
}

  • WordsCount类:各单词出现频率统计类,通过使用此类的方法,返回得到文本中的单词与出现个数的数据结构
package com.zzjAndcyy;

import java.util.HashMap;

public class WordsCount {

    /**
     * 使用HashMap集合
     * key保存单词,使得其保证唯一性
     * value 存储该key(即单词)的次数
     */
    private HashMap<String, Integer> map = new HashMap<>();

    /**
     * 根据题意
     * 存储字母数大于4的单词 所在文章中出现的次数
     */
    private int sum = 0;

    /**
     * 提供接口,返回sum的值
     * @return
     */
    public int getSum() {
        return sum;
    }

    /**
     * 提供接口,返回最后的HashMap查询结果
     * @return
     */
    public HashMap<String, Integer> getMap() {
        return map;
    }

    public WordsCount(String content) {
        // 构造正则表达式,去根据空格拆分整篇文章
        // temp字符串数组将保存所有的单词
        String[] temp = content.split("\\s+");

        // 根据题意
        // 构造正则表达式筛选规则:以字母开头且长度大于4的单词
        String reg = "^[a-zA-Z]{4,}.*";

        // 循环遍历这个数组,利用正则表达式去匹配
        for (String i : temp) {
            if (i.matches(reg)) {
                // 匹配成功,计数加1
                sum++;
                // 根据题意,不区分大小写,所以统一转换成小写
                String smallW = i.toLowerCase();
                // 如果HashMap中没有这个key,则说明第一次出现该单词,放入集合,记录频率为1
                if (!map.containsKey(smallW)) {
                    map.put(smallW, 1);
                } else {
                    // 到这里说明HashMap中有这个key,将其出现次数累加即可
                    int num = map.get(smallW);
                    map.put(smallW, num + 1);
                }
            }
        }
    }

}

  • CalMost类:将结果按照题目的要求排序,从高频词到低频次,如果相同,则按照字典顺序排列
package com.zzjAndcyy;

import java.util.*;

/**
 * 返回包含单词和它数量的HashMap
 */
public class CalMost {

    /**
     * 传入已经统计完全的map集合
     * 拿到排列后的所有单词数
     * 通过List 返回 前十个高频词 即可
     * @param map
     * @return
     */
    public List<Map.Entry<String, Integer>> mostWords(HashMap<String, Integer> map) {
        // 将map中的所有键值对给新的集合list
        List<Map.Entry<String, Integer>> list = new ArrayList<>(map.entrySet());
        // 利用自己写的比较器,去根据单词的频率去排序
        list.sort(new MapComparator());
        // 返回list 前十个 数据,也即出现的前十的高频词
        return list.size() < 10 ? list.subList(0, list.size()) : list.subList(0, 10);
    }


    /**
     * 新建一个内部类 MapComparator 比较器
     * 重写比较方法,使得能够让数据正确比较
     */
    private class MapComparator implements Comparator<Map.Entry<String, Integer>> {
        // 重写接口的比较方法,使得能够达到我们想要的比较效果
        // 首先根据频率比较,如果频率相同,比较字典顺序
        @Override
        public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
            return o1.getValue().compareTo(o2.getValue()) != 0 ? o2.getValue().compareTo(o1.getValue()) : o1.getKey().compareTo(o2.getKey());
        }
    }
}

从0到最后能够完成需求,我和陈远杨互相承担了很多,比如在遇到先去筛选前10高频词,然后如果频率相同用字典排序,我们就在想能不能用一个比较器类就达到效果。最终,两人通过题目的需求和查找相关资料实现了这个功能。

public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
            return o1.getValue().compareTo(o2.getValue()) != 0 ? o2.getValue().compareTo(o1.getValue()) : o1.getKey().compareTo(o2.getKey());
}

四、代码的复审

1.关于文本标点符号,特殊字符的处理

第一次代码完工过后,通过测试简单的样例,我们以为达到了最后的要求,从网上下载了泰戈尔的《飞鸟集》进行测试。没想到就翻车了,比如把 love和love.识别为两个单词。

我们知道可以修改通过正则表达式或者最开始处理一下文本去修改。

 String content = stringBuilder.toString()
                    .replace(".", " ")
                    .replace(",", " ")
                    .replace("!", " ")
                    .replace("?", " ");

2.性能优化

从一开始的将文本读入使用String来存储,发现性能实在是优化的空间太大。修改为StringBuilder去存储。

存字符串,Java有三种常规的方式,String、StringBuffer、StringBuilder

String一般用来存储常量更相对方便,底层实现是不可修改的char数组,导致每一次String的变化,都会使得新建一个String,而不能使得String动态改变。性能将会下降,在这一点上,是我们放弃String的原因。

StringBuffer和StringBuiler的底层char数组都可以动态变化, 区别在于很多方法是否加锁,StringBuffer更多的用于多线程,因为它的很多方法加了同步锁,而StringBuiler虽然没有同步机制,但更加适用于单线程,效率性能更快。

所以我们最后选择了StringBuiler进行字符的处理。

五、运行测试

我们首先进行飞鸟集的测试:

可以看到,结果很正确,那么我们就在思考,我们的程序统计的极限是哪里呢?

陈远杨同学采用了13MB大小的TXT文档做测试

陈远杨同学说:

程序在经过大概3S左右的时间后才得出结果,花费时间这么久肯定某个地方耗时超出了预期,100W个单词1300W个字母。并且选取的是飞鸟集和1984 的混合素材,可以看出距离和爱出现的频率很高(很符合飞鸟集2333)

六、性能分析

JProfiler是用于分析J2EE软件性能瓶颈并能准确定位到Java类或者方法有效解决性能问题的主流工具,它通常需要与性能测试工具如:LoadRunner配合使用,因为往往只有当系统处于压力状态下才能反映出性能问题。

所以我采用JProfiler进行性能测试。

可以看到性能的影响并不大,毕竟只是一个单线程程序。

七、单元测试

行数单元测试

08

字符单元测试

09

单词频率统计

10

八、附加功能

窗体程序本来并没有这个打算,因为熟悉了C#的窗体设计的朋友就知道,C#直接拖控件写代码多么便利。而Java则要自己使用Swing工具包去调整控件的大小,位置,达到相对自己满意的地步。

即便如此,为了认真对待这门课,我们毅然决然还是搞定Java的窗体程序。

运行主类,主界面如下显示

分析文本输入

测试上传文件

注意:上传文件将会把文件上传到程序目录下,所以再次上传同样的文件将会提示 文件已经存在

17

测试指令

-i 和 -n 指令的混合

总结

从看到了题目,我们便初步决定的首要目标,是先计划好主要有哪几个类。因为从本次的题目要求来看,功能需求很多,并且使用的是面向对象语言,那么动工写代码之前一定要规划化大概有多少个类,类担任的职责有哪些。

虽然提前规划的不一定会完美达到要求,但是也是利大于弊的。我们先来谈谈弊:

计划的太简单:

一开始的想法是既然就是完成统计一个文本中的字符数目情况,那么就可以拆分为两部分:运行主类+工具类。

  • 运行主类:提供程序的入口,找到我们需要处理的文本
  • 工具类,内含按照题目要求统计各种数目的方法,提供给运行主类调用

然后我们就开始动工了,我建立整个项目,包括从github上fork->拉取项目到本地->建立基本的运行环境->编写运行主类

陈远杨同学则进行编写较为复杂的工具类。

这时问题就出现了,代码风格不够统一,各自类的实现都有自己的见解,使得整个程序看起来很乱。

不过也得益于结对编程的利,互相检查出了很多以前没有注意到的错误,收获了很多可以改进的地方。所以,我们第一次编程完毕后,统一意见,将冗杂了各种方法的工具类提取出来分为多个功能明确的类,使得整个程序结构更为清晰。然后性能分析一起去找到了更为提高性能的一些改进,比如使用StringBuilder类...

综上所述:1+1在大家统一意见付之行动的情况下,确实>2.

posted @ 2019-10-11 10:42  etron  阅读(247)  评论(2编辑  收藏  举报