第一次个人编程作业

1. 作业github链接

https://github.com/Percy-Li-arch/Percy-Li-arch/blob/main/3124004097/main.jar

2. PSP表格记录

image

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 五个单元测试类,对每个类逐点覆盖。

关键函数流程图

image

image

算法的关键
  1. 预处理统一字符流:clean 用 replaceAll("\s+","") 把空格、换行、制表符全部抹掉,使文本变成连续字符序列。这一步保证"仅空格/换行不同"的两段文本会被判为完全相同(测试用例 9、11 验证),消除了排版差异带来的伪差异。

  2. 字符级 N-gram(bigram)作为特征单元:以相邻 2 个字符为一个 gram,不依赖分词器。这是中文场景下的关键取舍——避免引入分词库及其误差,任意语言/符号文本都能切分(测试用例 7 含特殊字符仍正确)。

  3. Jaccard 相似度做最终度量:用两文本 N-gram 集合的"交集/并集"量化重合程度,结果天然落在 [0,1] 区间,直接对应"重复率"语义。

  4. 完备的边界处理:空文本、单字符(长度 < N 无法成 gram)、双空等情况都有显式分支兜底,不会出现除零或误判。

独到之处
  1. 零外部依赖:不引入 HanLP、分词器或任何模型,仅用 JDK 自带的 HashSet、String 即完成查重,部署即运行,jar 包体积极小。

  2. 对"增删改"有较强抗干扰性:N-gram + Jaccard 的本质是衡量集合重合度,局部改词、增删句子只会改变少量 gram,重复率随之平滑下降,天然契合论文/作业查重中"改头换面式抄袭"的识别。

  3. 接口与实现彻底解耦:Main 只认一个 calculateSimilarity 接口,算法内部如何切分、N 取几都被封装,未来把 N=2 调成 N=3 或换成其他算法都不影响调用方。

  4. 异常语义化:把"参数错、文件找不到、写入失败"拆成三个独立异常类型,错误可精确定位,配合 MainTest 逐条断言,保证了模块的健壮性。

4. 模块接口部分的性能改进

改进花费的时间

本次性能改进共花费约2小时,其中性能分析定位约30分钟,代码改进约1小时,验证测试约30分钟

改进思路
  1. 减少重复的 N-gram 生成。原实现中每次调用 calculateSimilarity() 都会重新对两段文本分别生成 N-gram 集合。改进思路是将 N-gram 生成逻辑缓存化,对于同一段文本只生成一次,后续直接复用结果。优化集合交并运算

  2. Jaccard 相似度计算中,求交集和并集需要遍历两个 HashSet。改进思路是:交集遍历时选择较小的集合进行迭代,减少遍历次数;并集大小直接通过 |A| + |B| - |A∩B| 计算,避免重复构建新集合。

  3. 预处理阶段优化。原文预处理时使用了多次 String.replaceAll() 进行正则替换,每次都会创建新字符串对象。改进思路是改用单次遍历的 StringBuilder 逐字符处理,减少中间对象的 GC 开销。

性能分析图

Flame Graph
image

Call Tree
image

程序中消耗最大的函数

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

5. 模块单元测试

部分单元测试代码

image

image

image

测试的函数说明

image

构造测试数据的思路

测试数据的构造遵循"正常路径 + 边界条件 + 异常路径"三层策略:

  1. 正常路径(验证功能正确性)
    完全相同文本:验证相似度为 1.00,确认算法对 identical 输入的正确性
    完全不同文本:验证相似度为 0.00,确认无交集时的行为
    部分相似文本:验证结果在 (0, 1) 区间内,确认 Jaccard 计算的连续性

  2. 边界条件(验证鲁棒性)
    空文本 / 双空:验证不会除零崩溃,双空返回 1.00
    单字符文本:长度 < N(bigram 的 N=2),无法生成 gram,验证兜底逻辑
    纯空白文本:预处理后等价于空文本,验证 clean() 与算法的协同
    仅排版不同(空格/换行差异):验证预处理能消除排版伪差异

  3. 异常路径(验证错误处理)
    文件不存在:验证 FileUtil.readFile() 抛出 FileNotFoundException
    参数数量错误:验证 Main 抛出 InvalidParameterException
    写入失败:验证 FileUtil.writeFile() 抛出 FileReadException

测试覆盖率图

image

6. 模块异常处理

InvalidParameterException

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

对应场景:用户执行 java -jar main.jar 时未传入任何参数。
image

FileReadException

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

对应场景: 用户指定的答案输出路径已经是一个目录(而非文件路径),导致程序无法写入结果文件。
image

FileNotFoundException

设计目标: 当用户指定的原文文件或抄袭文文件在文件系统中不存在时,抛出此异常,明确告知用户是输入文件缺失导致的错误,而非程序本身故障。

对应场景: 用户传入的抄袭文路径对应的文件不存在(如拼写错误或文件未创建)。
image

posted @ 2026-09-12 10:00  李沛熹  阅读(24)  评论(0)    收藏  举报