作业 GitHub 仓库:https://github.com/ccjfd/3124004430
| 这个作业属于哪个课程 | 软件工程 |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702 |
| 这个作业的目标 | 使用 Java 完成支持命令行文件输入输出的论文查重程序。 |
| 学号 | 3124004430 |
一、PSP 表格
表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 25 |
| · Estimate | 估计这个任务需要多少时间 | 10 | 10 |
| Development | 开发 | 430 | 480 |
| · Analysis | 需求分析(学习 jieba 分词、余弦相似度) | 60 | 70 |
| · Design Spec | 生成设计文档 | 30 | 35 |
| · Design Review | 设计复审 | 15 | 15 |
| · Coding Standard | 代码规范 | 15 | 15 |
| · Design | 具体设计 | 40 | 45 |
| · Coding | 具体编码 | 150 | 170 |
| · Code Review | 代码复审 | 30 | 30 |
| · Test | 测试(自我测试,修改代码,提交修改) | 90 | 100 |
| Reporting | 报告 | 120 | 135 |
| · Test Report | 测试报告 | 60 | 70 |
| · Size Measurement | 计算工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 50 | 55 |
| 合计 | 570 | 640 |
二、计算模块接口的设计与实现过程
2.1 整体流程
2.2 类与职责划分
项目共 7 个主类 + 2 个自定义异常类,遵循单一职责原则,每层都可以独立单元测试:
表格
| 类 | 职责 |
|---|---|
| Main | 命令行入口,解析 3 个参数(原文 / 抄袭版 / 答案路径),约定退出码(0 成功 / 1 参数错误 / 2 运行异常) |
| DuplicateChecker | 流程组装层,串联 读文件→分词→向量化→算相似度 |
| TextReader | 文件读取。先按 UTF-8 严格解码,失败回退 GBK,自动剔除 UTF-8 BOM |
| Tokenizer | 基于 jieba(SEARCH 模式)分词,过滤空白 token,英文统一转小写 |
| VectorBuilder | 把词列表转换为 Map<String, Integer> 词频向量 |
| SimilarityCalculator | 计算余弦相似度,结果钳位到 [0,1] |
| FileReadException | 文件不存在 / 不可读时抛出 |
| EmptyContentException | 文本为空、无法计算相似度时抛出 |
2.3 算法关键与独到之处
算法核心:把两篇论文分别表示为词频向量,用余弦相似度度量重复率。分母为两向量模长的乘积,分子为点积。设两篇文档词数分别为 n1、n2,时间复杂度为 O (n1 + n2)。
三个独到设计:
- 点积只遍历较小向量:在较小向量上迭代、到较大向量中 get 查找公共词,避免对全词表做集合求交,常数因子更小。
- 编码自适应:测试文本可能来自不同编辑器,编码不统一。TextReader 用严格模式 UTF-8 解码(非法字节立即抛错而非静默替换),失败后回退 GBK,保证任何来源的 txt 都能正确读取。
- 空白 token 过滤:分词后剔除纯空白 token,避免换行 / 空格数量差异干扰重复率;保留标点参与统计,使标点改动也被计入。
已知局限(改进方向):词频向量与语序无关,"我打了他" 和 "他打了我" 会得到相同结果。可改进为字符 n-gram 或 SimHash以引入位置信息。
三、计算模块接口部分的性能改进
3.1 性能测量
用真实测试文本(约 1 万字中文)实测,程序端到端耗时约 1.7 ~ 2.3 秒,其中:
表格
| 阶段 | 耗时 | 占比 |
|---|---|---|
| JVM 启动 | ~0.2s | ~10% |
| jieba 词典加载 | ~0.5s | ~25%(最大瓶颈) |
| 分词(两篇文档) | ~0.9s | ~50% |
| 词频统计 + 余弦计算 | <0.05s | ~2% |
3.2 改进思路
- 分词器实例进程内共享(已实现):Tokenizer 中用 static final 单例持有 JiebaSegmenter,词典只加载一次;
- 点积遍历较小向量、模长用平方和累加(已实现);
- 若还需提速:可关闭 jieba 的 HMM 新词发现、用 JMH 定位热点后针对性优化;
- 内存方面:两篇文档的词表按需构建,占用量远低于 2048MB 限制,5 秒时限内可稳定给出答案。
四、计算模块部分单元测试展示
测试框架 JUnit 5,共 19 个测试用例,全部通过,采用白盒方式按 "纯函数层 → 流程层 → 命令行层" 构造:
表格
| 测试类 | 测试内容 | 构造思路 |
|---|---|---|
| TestTokenizer | 分词不含空白 token;空文本抛 EmptyContentException;英文转小写 | 白盒:检查过滤边界与归一化逻辑 |
| TestVectorAndCosine | 词频累加正确;相同向量 = 1.0;无公共词 = 0.0;部分重叠∈(0,1) | 边界值三角法:上界 / 下界 / 中间态 |
| TestTextReader | UTF-8 正常读取;GBK 兼容读取;文件不存在抛异常 | 用 @TempDir 临时目录分别写两种编码的文件 |
| TestCheckerFlow | 相同文本 = 1.00;题目样例 > 0.5;完全不同文本 < 0.1;空文件抛异常 | 题目样例 + 极端样本对照 |
| TestFileIOAndMain | 完整流程答案格式为 \d.\d {2};参数错误退出码 1;缺文件 / 空文件退出码 2;%.2f 格式化 | 验证退出码契约与输出格式契约 |
用 mvn test(JaCoCo 插件)统计覆盖率,报告在 target/site/jacoco/index.html:
表格
| 指标 | 覆盖率 |
|---|---|
| 指令覆盖 | 81% |
| 行覆盖 | 80% |
| 分支覆盖 | 71% |

测试设计评价:19 个用例覆盖了分词边界、向量计算边界、双编码读取、空输入、缺失文件、参数错误等主要路径,行覆盖 80%。未覆盖部分主要是 Main 的 System.exit 分支与 GBK 回退的深层分支,属于防御性代码;若追求更高覆盖可补充非法字节序列的构造测试。总体能满足程序功能测试要求。
五、计算模块部分异常处理说明
表格
| 异常 | 设计目标 | 错误场景 | 对应单元测试 |
|---|---|---|---|
| FileReadException | 文件不存在或不可读时给出明确中文提示,而不是抛出底层 IOException 栈 | 评测命令中传入了错误路径:java -jar main.jar C:\notexist.txt ... | readNotExists、mainMissingFile |
| EmptyContentException | 任一文件为空(或分词后无有效内容)时提前拦截,避免输出误导性结果或除零错误 | 答案前忘了写内容:type nul > empty.txt 后把 empty.txt 当作抄袭版传入 | tokenizeEmptyThrows、emptyFileThrows、mainEmptyFile |
| 参数个数错误 | 打印用法提示并以退出码 1 结束 | 评测机传参不足 3 个 | mainWrongArgs |
退出码约定:0 成功 / 1 参数错误 / 2 运行期异常,评测机可据此判断程序是否正常退出。
六、实测结果
用提供的测试文本:
表格
| 测试点 | 输出 |
|---|---|
| orig_0.8_add.txt(噪声增字版) | 1.00 |
| orig_0.8_del.txt(删字版) | 0.99 |
| orig_0.8_dis_1.txt(乱序扰动 1) | 1.00 |
| orig_0.8_dis_10.txt(乱序扰动 10) | 1.00 |
| orig_0.8_dis_15.txt(乱序扰动 15) | 0.99 |
结果符合词频余弦模型的预期:对增删改不敏感、与语序无关;全部测试点均在 5 秒内返回。
七、事后总结与改进计划
本次作业预估总耗时 570 分钟,实际总耗时 640 分钟,比预估多出 70 分钟。
差距主要原因:
- 一开始对
jieba-analysisJava 分词库 API 不熟,踩坑较多,比如词典加载重复、分词结果出现大量空白符号,调试花了不少时间; - 文件编码兼容处理比预想麻烦,Windows 下 GBK、UTF-8 带 BOM 多种文本要兼容,反复构造测试文件验证;
- 单元测试和异常分支编写、JaCoCo 覆盖率调试花费时间超预期,很多边界场景(空文件、非法路径)要单独写测试用例。
本次收获:
- 学会按 PSP 流程拆分任务,提前预估时间,开发、测试、文档分开,不是写完代码再一次性补报告;
- 理解了余弦相似度查重的优缺点:简单高效,但无法识别语序改动;
- 学会单元测试、代码覆盖率工具,提前捕获边界异常,而不是等到程序上线报错。
后续改进计划:
- 算法层面:后续可以升级 SimHash 算法,加入文本语序信息,解决词频向量无法区分句子颠倒的缺陷;
- 性能层面:预加载 jieba 词典做全局单例,减少重复加载开销;关闭 HMM 新词识别,进一步提升分词速度;
- 工程层面:增加日志模块,方便定位异常;补充更多极端测试用例,提升分支覆盖率;
- 开发流程:下次开发前先写单元测试(TDD),先定义接口,再写实现,减少后期返工。
浙公网安备 33010602011771号