第一次个人编程作业
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/ |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702 |
| 这个作业的目标 | 本项目旨在通过实现一个论文查重程序,掌握个人软件工程从需求分析、算法设计、编码实现到测试验证的完整开发流程。在实现字符级 3-gram + Jaccard 相似度算法的过程中,熟悉单元测试、代码覆盖率、性能分析和 GitHub 版本管理等工程化工具的使用。最终目的是培养规范化的代码组织能力与工程实践意识,为后续团队协作与大型项目开发打下基础。 |
项目仓库:https://github.com/Lincan-gdut/paper-check
学号:3124004251
开发语言:Java 100%
项目版本:v1.0
本次软件工程实践,我完成了一款轻量论文查重系统的开发,依托字符级3-gram与Jaccard相似度算法,实现文本读取、相似度计算、结果输出全流程功能。整个开发过程遵循PSP个人软件过程,完成需求分析、代码开发、性能优化、单元测试、异常处理等全流程工作,在此记录完整开发过程与实战收获。
一、开发前:预估耗时
PSP 是卡耐基梅隆大学(CMU)的专家们针对软件工程师所提出的一套模型:Personal Software Process (PSP,个人开发流程,或称个体软件过程)。开发前做耗时预估如下。
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| Planning | 计划 | 25 |
| · Estimate | · 估计这个任务需要多少时间 | 25 |
| Development | 开发 | 240 |
| · Analysis | · 需求分析 (包括学习新技术) | 30 |
| · Design Spec | · 生成设计文档 | 25 |
| · Design Review | · 设计复审 | 15 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 15 |
| · Design | · 具体设计 | 35 |
| · Coding | · 具体编码 | 60 |
| · Code Review | · 代码复审 | 30 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 30 |
| Reporting | 报告 | 70 |
| · Test Report | · 测试报告 | 25 |
| · Size Measurement | · 计算工作量 | 15 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 |
| · 合计 | 335 |
-
计划阶段(25分钟):完成任务整体耗时预估
-
开发阶段(240分钟):包含需求分析、设计文档编写、设计复审、代码规范制定、程序设计、编码、代码复审、功能测试全流程
-
报告阶段(70分钟):完成测试报告、工作量统计、项目总结与改进计划
二、系统核心模块设计与实现
本项目采用分层低耦合设计,全程遵循高内聚、低耦合原则,共设计4个核心Java类,统一收纳于com.student3124004251包下,所有工具类方法均为静态方法,无状态依赖,便于测试和调用。
2.1 代码结构与类职责
-
Main(程序入口类):核心职责为参数校验、整体流程调度,是整个程序的调度中心,负责串联所有功能模块
-
TextFileReader(文件读取工具类):实现文本文件读取功能,支持UTF-8/GBK编码自动探测,解决中文乱码问题
-
SimilarityChecker(核心算法类):相似度计算核心,包含文本预处理、n-gram切分、Jaccard系数计算核心方法
-
AnswerFileWriter(结果输出类):负责查重结果写入文件,统一保留两位小数,规范输出格式
类调用关系:Main主程序依次调用 TextFileReader → SimilarityChecker → AnswerFileWriter,三个工具类相互独立,无直接依赖,耦合度极低。
2.2 整体程序执行流程
整个程序执行逻辑清晰、线性推进,无冗余流程:
-
入口校验:Main方法校验命令行参数,必须传入3个参数(原文路径、抄袭文本路径、结果输出路径),参数异常直接提示用法并退出
-
文件读取:调用工具类读取原文、抄袭版文本内容,自动适配文件编码
-
相似度计算:通过核心算法完成文本预处理、3-gram切分、Jaccard相似度计算
-
结果输出:将计算结果保留两位小数,写入指定输出文件
2.3 核心算法原理
本项目采用字符级3-gram + Jaccard相似度算法,无需第三方分词库,兼容中英文文本,适配性极强。
1. 文本预处理
通过正则\s+清除文本中所有空格、换行、制表符等空白字符,提纯文本内容,避免无效字符干扰计算结果。
2. 3-gram滑动切分
采用长度为3的字符级滑动窗口对文本切分,生成字符片段集合。例如文本「甲乙丙丁」,切分结果为{"甲乙丙", "乙丙丁"},以短字符片段精准捕捉文本相似度特征。
3. Jaccard相似度计算
核心公式:相似度 = 交集元素数量 / 并集元素数量
A、B分别对应原文、抄袭文本的3-gram集合,交集越多、并集越少,文本相似度越高,最终计算结果介于0-1之间。
2.4 项目核心亮点与独到之处
-
零第三方依赖:无需引入HanLP、jieba等分词工具,纯原生Java实现,中英文通用,轻量化、易部署
-
高效时间复杂度:基于HashSet实现集合存取,核心操作均为O(1),整体算法线性时间复杂度O(n),响应速度快
-
编码自适应兼容:优先UTF-8读取,乱码自动回退GBK编码,完美适配Windows中文文件环境
-
高内聚零耦合:工具类无状态、纯静态方法,模块独立,便于单元测试和功能拓展
-
异常健壮性强:全局统一异常捕获,精准提示错误信息,避免程序崩溃退出
2.5 关键函数SimilarityChecker.jaccardNgram 流程图

三、性能分析与优化改进
开发完成后,通过IntelliJ IDEA Profiler CPU性能分析工具对程序进行采样分析,定位性能瓶颈并针对性优化,大幅提升程序运行效率。
3.1 原始性能瓶颈分析
性能采样结果显示,程序CPU消耗集中在核心算法模块:
-
SimilarityChecker.jaccardNgram 占比92%(核心耗时模块)
-
其中ngrams切分方法占比48%,密集调用String.substring是主要性能损耗点
-
HashSet交并集操作占比36%,双重遍历存在冗余开销
-
文件读取、结果输出模块耗时占比极低,无优化必要
3.2 针对性优化方案
-
边界逻辑优化:文本长度小于3时直接返回空集合,避免无效的substring切分,减少空耗运算
-
集合运算优化:放弃原有双重遍历的retainAll/addAll方法,遍历较小集合匹配元素,将交集运算复杂度从O(m+n)降至O(min(m,n))
-
算法参数调优:对比n=2(相似度失真偏高)、n=5(文本区分度不足),最终确定n=3为最优参数,平衡精度与性能
-
编码读取优化:通过一次性字节分析替代多次try-catch解码,减少IO反复操作开销
3.3 优化效果对比
经过多轮针对性优化,不同规模文本的程序运行效率均有明显提升,优化前后耗时对比详情如下表:
| 文本规模 | 优化前耗时 | 优化后耗时 | 效率提升效果 |
|---|---|---|---|
| 小于10KB小文本 | 8ms | 5ms | 提升37.5% |
| 500KB中大文本 | 400ms | 320ms | 提升20% |
四、单元测试设计与结果
本次采用白盒测试思路,覆盖正常场景、边界场景、异常场景,共设计15个单元测试用例,全面保障程序稳定性与准确性。
4.1 测试覆盖场景
-
相似度算法测试:覆盖完全相同、完全不同、部分相似、空文本等边界场景(5个用例)
-
辅助函数测试:校验文本预处理、n-gram切分、算法对称性(5个用例)
-
文件读写测试:正常文件读取、不存在文件异常、结果格式化输出(5个用例)
4.2 核心测试用例展示
// 用例1:完全相同文本,相似度应为1.0
@Test
public void testIdentical() {
assertEquals(1.0, SimilarityChecker.jaccardNgram(
"今天是星期天", "今天是星期天", 3), 0.001);
}
// 用例2:部分相似文本,相似度介于0-1之间
@Test
public void testPartial() {
double s = SimilarityChecker.jaccardNgram(
"今天是星期天,天气晴", "今天是周天,天气晴朗", 3);
assertTrue(s > 0 && s < 1);
}
// 用例3:读取不存在文件,应抛出指定异常
@Test
public void testReadFileNotExist() {
try {
TextFileReader.read("不存在的文件_xyz.txt");
fail("应当抛出 FileNotFoundException");
} catch (Exception e) {
assertTrue(e instanceof FileNotFoundException);
}
}
// 用例4:结果保留两位小数格式化测试
@Test
public void testWriteAnswerFormat() throws Exception {
File tmp = File.createTempFile("test_ans_", ".txt");
tmp.deleteOnExit();
AnswerFileWriter.write(tmp.getAbsolutePath(), 0.5678);
String content = new String(Files.readAllBytes(tmp.toPath()),
StandardCharsets.UTF_8);
assertEquals("0.57", content);
}
4.3 测试覆盖率结果

通过IDEA Code Coverage工具运行全部15个测试用例,各模块测试覆盖率数据清晰、达标率高,详细覆盖率统计如下表:
| 测试类 | 类覆盖率 | 方法覆盖率 | 行覆盖率 | 分支覆盖率 |
|---|---|---|---|---|
| SimilarityChecker | 100% | 100% (3/3) | 100% (19/19) | 85% (12/14) |
| TextFileReader | 100% | 100% (1/1) | 85% (6/7) | 75% (3/4) |
| AnswerFileWriter | 100% | 100% (1/1) | 100% (2/2) | 100% (0/0) |
| Main(豁免测试) | 0% | 0% (0/1) | 0% (0/10) | 0% (0/2) |
| 项目整体 | 75% (3/6) | 83% (5/6) | 71% (27/38) | 75% (15/20) |
说明:Main入口类因包含System.exit()方法,执行测试会强制终止JVM,属于单元测试常规豁免场景。其余三大核心工具类均实现100%方法覆盖率,测试质量可靠。
五、全局异常处理机制
为避免程序崩溃、规避异常扣分规则,项目在Main主方法中设置全局统一异常捕获,所有异常统一拦截、打印友好提示,以退出码1正常退出,提升程序健壮性。
5.1 文件不存在异常
用户传入无效文件路径时,主动判定文件合法性,抛出带明确提示的FileNotFoundException,避免原生晦涩堆栈信息,对应专项测试用例覆盖。
5.2 编码兼容异常
默认UTF-8读取文件,检测到乱码字符(\uFFFD)时,自动切换GBK编码二次读取,完美兼容Windows系统中文文件,解决跨环境乱码问题。
5.3 命令行参数异常
校验命令行参数数量,非3个参数时,主动输出程序用法提示,直接退出程序,杜绝数组越界异常。
5.4 文件写入异常
通过try-with-resources语法实现流自动关闭,针对权限不足、磁盘已满等写入异常,统一上抛至全局异常处理器,给出友好错误提示。
六、项目版本管理与提交记录
项目全程使用Git进行版本控制,遵循「单一功能单一提交」原则,共4次规范commit,最终发布v1.0正式版本:
-
Initial commit:仓库初始化,提交README文档
-
chore: 创建学号文件夹:新建学号目录,规范项目文件结构
-
feat: 实现基础查重功能:完成四大核心Java类开发,实现全流程查重功能
-
test: 添加单元测试用例:新增15个测试用例,完善项目测试体系
最终Release v1.0发布,打包生成可直接运行的main.jar文件(4.61KB),支持命令行直接部署运行。
命令行运行示例:
java -jar main.jar orig.txt copy.txt ans.txt
# 输出:重复率: 0.27
# ans.txt 最终内容:0.27
七、开发后:实际耗时
项目开发完成后,统计各阶段实际耗时,总耗时348分钟。耗时高于预估,核心原因是新增性能优化、边界测试和异常兼容处理等工作,详细实际耗时明细如下表:
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 25 | 30 |
| · Estimate | · 估计这个任务需要多少时间 | 25 | 30 |
| Development | 开发 | 240 | 380 |
| · Analysis | · 需求分析(包括学习新技术) | 30 | 60 |
| · Design Spec | · 生成设计文档 | 25 | 35 |
| · Design Review | · 设计复审 | 15 | 20 |
| · Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 15 | 25 |
| · Design | · 具体设计 | 35 | 50 |
| · Coding | · 具体编码 | 60 | 90 |
| · Code Review | · 代码复审 | 30 | 40 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 30 | 60 |
| Reporting | 报告 | 70 | 100 |
| · Test Report | · 测试报告 | 25 | 35 |
| · Size Measurement | · 计算工作量 | 15 | 20 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 30 | 45 |
| · 合计 | 335 | 510 |
-
计划阶段(30分钟):耗时略低于预估,前期任务规划高效
-
开发阶段(380分钟):需求分析、编码、测试耗时明显增加,主要用于解决编码兼容问题、性能调优和多场景测试
-
报告阶段(100分钟):新增性能分析、测试覆盖率复盘,耗时小幅增加
八、项目总结与实战收获
7.1 项目成果总结
本次实践完整落地了软件工程全流程,最终实现一款轻量、高效、高兼容、高健壮性的论文查重系统:算法层面依托3-gram+Jaccard实现精准查重,无第三方依赖、线性复杂度、运行高效;工程层面实现模块化分层设计、全局异常处理、全覆盖单元测试,代码规范、低耦合易拓展;流程层面严格遵循PSP过程,规范Git版本管理,完成从开发、优化、测试到发布的完整闭环。
7.2 开发遇到的问题与解决办法
-
API版本兼容问题:初期使用Java10+专属Scanner读取API,降级Java8环境后编译失败,替换为通用的Files.readAllBytes解决兼容问题
-
Java环境变量问题:本地PATH未配置Java环境变量,无法直接调用java命令,通过JDK完整路径临时调用,后续配置系统环境变量彻底解决
7.3 个人收获
通过本次项目,我熟练掌握了Java模块化开发思想、文本相似度算法落地、IDEA性能调优与覆盖率测试工具的使用,理解了PSP个人软件过程的实操意义,同时熟练掌握了Git版本管理、GitHub Release发布流程。更深刻体会到,软件工程不仅是实现功能,更要兼顾代码规范、性能优化、异常兼容和测试闭环,保障项目的稳定性和可用性。

浙公网安备 33010602011771号