第一次个人编程作业
1. 作业github链接
https://github.com/Percy-Li-arch/Percy-Li-arch/blob/main/3124004097/main.jar
2. PSP表格记录

3. 模块接口的设计与实现过程
代码如何组织
整个计算模块按"单一职责"原则拆成 4 个功能类 + 3 个自定义异常类,分属两个包:com.checker(业务逻辑)和 com.checker.exception(异常定义)。
| 类名 | 所属包 | 职责 | 关键函数 |
|---|---|---|---|
Main |
com.checker | 程序入口:校验参数 → 读文件 → 调算法 → 写结果 | main(String[] args) |
CheckAlgorithm |
com.checker | 核心算法:把相似度计算封装为对外接口 | calculateSimilarity()(对外)、generateNgrams()(内部) |
TextPreprocessor |
com.checker | 文本预处理:清洗空白,统一为连续字符流 | clean() |
FileUtil |
com.checker | 文件读写工具,屏蔽 IO 细节 | readFile()、writeFile() |
InvalidParameterException |
com.checker.exception | 参数数量非 3 时抛出 | — |
FileNotFoundException |
com.checker.exception | 输入文件不存在 / 不可读时抛出 | — |
FileReadException |
com.checker.exception | 答案文件写入失败时抛出 | — |
类之间的调用关系
模块整体是一条单向、无环的调用链,依赖方向清晰、互不耦合:
Main(入口/调度)
├─> FileUtil.readFile() 读取原文、抄袭版
├─> CheckAlgorithm.calculateSimilarity() 计算重复率
│ ├─> TextPreprocessor.clean() 分别清洗两段文本
│ └─> generateNgrams() 内部生成 N-gram 集合并求 Jaccard
└─> FileUtil.writeFile() 写出答案(保留两位小数)
异常类由 Main 在捕获 IO 错误后按需抛出,向上汇报错误原因。测试层另设 MainTest / CheckAlgorithmTest / FileUtilTest / TextPreprocessorTest / ExceptionTest 五个单元测试类,对每个类逐点覆盖。
关键函数流程图


算法的关键
-
预处理统一字符流:clean 用 replaceAll("\s+","") 把空格、换行、制表符全部抹掉,使文本变成连续字符序列。这一步保证"仅空格/换行不同"的两段文本会被判为完全相同(测试用例 9、11 验证),消除了排版差异带来的伪差异。
-
字符级 N-gram(bigram)作为特征单元:以相邻 2 个字符为一个 gram,不依赖分词器。这是中文场景下的关键取舍——避免引入分词库及其误差,任意语言/符号文本都能切分(测试用例 7 含特殊字符仍正确)。
-
Jaccard 相似度做最终度量:用两文本 N-gram 集合的"交集/并集"量化重合程度,结果天然落在 [0,1] 区间,直接对应"重复率"语义。
-
完备的边界处理:空文本、单字符(长度 < N 无法成 gram)、双空等情况都有显式分支兜底,不会出现除零或误判。
独到之处
-
零外部依赖:不引入 HanLP、分词器或任何模型,仅用 JDK 自带的 HashSet、String 即完成查重,部署即运行,jar 包体积极小。
-
对"增删改"有较强抗干扰性:N-gram + Jaccard 的本质是衡量集合重合度,局部改词、增删句子只会改变少量 gram,重复率随之平滑下降,天然契合论文/作业查重中"改头换面式抄袭"的识别。
-
接口与实现彻底解耦:Main 只认一个 calculateSimilarity 接口,算法内部如何切分、N 取几都被封装,未来把 N=2 调成 N=3 或换成其他算法都不影响调用方。
-
异常语义化:把"参数错、文件找不到、写入失败"拆成三个独立异常类型,错误可精确定位,配合 MainTest 逐条断言,保证了模块的健壮性。
4. 模块接口部分的性能改进
改进花费的时间
本次性能改进共花费约2小时,其中性能分析定位约30分钟,代码改进约1小时,验证测试约30分钟
改进思路
-
减少重复的 N-gram 生成。原实现中每次调用 calculateSimilarity() 都会重新对两段文本分别生成 N-gram 集合。改进思路是将 N-gram 生成逻辑缓存化,对于同一段文本只生成一次,后续直接复用结果。优化集合交并运算
-
Jaccard 相似度计算中,求交集和并集需要遍历两个 HashSet。改进思路是:交集遍历时选择较小的集合进行迭代,减少遍历次数;并集大小直接通过 |A| + |B| - |A∩B| 计算,避免重复构建新集合。
-
预处理阶段优化。原文预处理时使用了多次 String.replaceAll() 进行正则替换,每次都会创建新字符串对象。改进思路是改用单次遍历的 StringBuilder 逐字符处理,减少中间对象的 GC 开销。
性能分析图
Flame Graph

Call Tree

程序中消耗最大的函数
性能分析结果显示,程序运行总耗时约 105ms,其中 Main.main() 方法占比 80.0%(84ms)。在该方法内部,通过火焰图可以进一步看到,消耗最大的函数是 CheckAlgorithm.calculateSimilarity(),其内部包含的 N-gram 生成(generateNgrams)和 HashSet 交并运算占用了绝大部分 CPU 时间。文件读取(FileUtil.readFile)次之,字符串格式化(String.format)等后处理操作耗时可忽略。
5. 模块单元测试
部分单元测试代码



测试的函数说明

构造测试数据的思路
测试数据的构造遵循"正常路径 + 边界条件 + 异常路径"三层策略:
-
正常路径(验证功能正确性)
完全相同文本:验证相似度为 1.00,确认算法对 identical 输入的正确性
完全不同文本:验证相似度为 0.00,确认无交集时的行为
部分相似文本:验证结果在 (0, 1) 区间内,确认 Jaccard 计算的连续性 -
边界条件(验证鲁棒性)
空文本 / 双空:验证不会除零崩溃,双空返回 1.00
单字符文本:长度 < N(bigram 的 N=2),无法生成 gram,验证兜底逻辑
纯空白文本:预处理后等价于空文本,验证 clean() 与算法的协同
仅排版不同(空格/换行差异):验证预处理能消除排版伪差异 -
异常路径(验证错误处理)
文件不存在:验证 FileUtil.readFile() 抛出 FileNotFoundException
参数数量错误:验证 Main 抛出 InvalidParameterException
写入失败:验证 FileUtil.writeFile() 抛出 FileReadException
测试覆盖率图

6. 模块异常处理
InvalidParameterException
设计目标: 当用户通过命令行启动程序时,如果传入的参数数量不足(少于 3 个:原文路径、抄袭文路径、输出路径)或参数格式不合法,抛出此异常,提示用户正确的使用方式。选择继承 RuntimeException 是因为参数校验属于程序启动阶段的逻辑错误,调用方无法恢复,应直接终止并提示。
对应场景:用户执行 java -jar main.jar 时未传入任何参数。

FileReadException
设计目标:当程序在读取输入文件或写入结果文件时遇到 I/O 错误(如文件不存在、路径是目录、无读写权限等),抛出此异常。该异常帮助程序在遇到文件系统问题时优雅终止,而非崩溃或产生不确定的行为。
对应场景: 用户指定的答案输出路径已经是一个目录(而非文件路径),导致程序无法写入结果文件。

FileNotFoundException
设计目标: 当用户指定的原文文件或抄袭文文件在文件系统中不存在时,抛出此异常,明确告知用户是输入文件缺失导致的错误,而非程序本身故障。
对应场景: 用户传入的抄袭文路径对应的文件不存在(如拼写错误或文件未创建)。


浙公网安备 33010602011771号