个人项目作业
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/SoftwareEngineeringClassof2023/ |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/SoftwareEngineeringClassof2023/homework/13324 |
| 这个作业的目标 | <学会查重算法设计与实现, 掌握单元测试和性能分析能力> |
https://github.com/16lab-adachi/16lab-adachi
一. 表格
PSP2.1 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 15 | 20 |
| · Estimate | · 估计这个任务需要多少时间 | 15 | 20 |
| Development | 开发 | 255 | 430 |
| · Analysis | · 需求分析 (包括学习新技术) | 30 | 100 |
| · Design Spec | · 生成设计文档 | 20 | 20 |
| · Design Review | · 设计复审 | 10 | 20 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 5 | 5 |
| · Design | · 具体设计 | 20 | 40 |
| · Coding | · 具体编码 | 120 | 180 |
| · Code Review | · 代码复审 | 20 | 20 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 30 | 45 |
| Reporting | 报告 | 55 | 70 |
| · Test Report | · 测试报告 | 20 | 25 |
| · Size Measurement | · 计算工作量 | 15 | 15 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 30 |
| 合计 | 325 | 520 |
二.模块设计
- 代码组织:
PaperChecker 类: 负责整个查重流程的控制。
readFileContent 方法: 读取文件内容。
writeSimilarityToFile 方法: 将相似度写入文件。
preprocessText 方法: 文本预处理(分词、去除停用词)。
bigramSplit 方法: 二元分词。
calculateSimilarity 方法: 计算相似度。
- 算法关键:
二元分词 (Bigram): 将文本分割成相邻的两个字符组成的词组。
去除停用词: 移除常见的、对相似度计算没有贡献的词(如 "的"、"了"、"是" 等)。
HashSet 存储: 使用 HashSet 存储原文的二元组,提高查找效率。
相似度计算: 使用 Jaccard 相似度公式的变体:共同出现的二元组数量 / (原文和抄袭版文本中较长的二元组数量)。
三.性能改进
- 改进思路:
主要优化点: bigramSplit 方法中的字符串操作。
优化方法: 使用 toCharArray 避免substring的开销

private List<String> bigramSplit(String text) {
// 参数校验
if (text == null) {
throw new NullPointerException("待分词文本为空");
}
List<String> bigrams = new ArrayList<>();
// 移除空格、全角空格和标点符号(更全面的 Unicode 标点符号)
text = text.replaceAll("[\\s\\p{Z}\\p{P}]+", "");
// 使用 StringBuilder 优化字符串拼接 (尤其重要,对于长文本)
int len = text.length();
// 如果文本长度小于2,直接返回空列表,避免不必要的循环
if (len < 2) {
return bigrams;
}
//直接使用char数组, 避免substring的开销
char[] chars = text.toCharArray();
for (int i = 0; i < len - 1; i++) {
// 提取二元组
//String bigram = text.substring(i, i + 2); //原始, 效率较低
bigrams.add(String.valueOf(chars, i, 2));
}
return bigrams;
}
四.单元测试
// 辅助方法:创建测试文件
private void createTestFile(String filePath, String content) throws IOException {
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) {
writer.print(content);
}
}
// 辅助方法:删除测试文件
private void deleteTestFile(String filePath) {
File file = new File(filePath);
if (file.exists()) {
file.delete();
}
}
// 测试用例1:正常情况下的相似度计算
// 目的:验证程序在正常输入(两个非空、内容不同的文本文件)下能否正确计算相似度。
// 预期结果:相似度值应在 0.0 到 1.0 之间,且不等于 0.0 或 1.0。
@Test
public void testCalculateSimilarity_normal() throws IOException {
// 准备测试数据
String originalText = "这是原始文本。";
String plagiarizedText = "这是抄袭文本!";
String originalFilePath = "original.txt";
String plagiarizedFilePath = "plagiarized.txt";
String answerFilePath = "answer.txt";
createTestFile(originalFilePath, originalText);
createTestFile(plagiarizedFilePath, plagiarizedText);
// 创建 PaperChecker 对象
PaperChecker checker = new PaperChecker(originalFilePath, plagiarizedFilePath, answerFilePath);
// 调用 calculateSimilarity 方法
double similarity = checker.calculateSimilarity();
// 断言:相似度应大于 0 且小于 1
assertTrue(similarity > 0.0);
assertTrue(similarity < 1.0);
// 清理测试文件
deleteTestFile(originalFilePath);
deleteTestFile(plagiarizedFilePath);
deleteTestFile(answerFilePath);
}
// 测试用例2:两个空文件的相似度计算
// 目的:验证程序在输入两个空文件时能否正确处理。
// 预期结果:相似度值应为 0.0。
@Test
public void testCalculateSimilarity_emptyFiles() throws IOException {
// 准备测试数据(空文件)
String originalFilePath = "empty_original.txt";
String plagiarizedFilePath = "empty_plagiarized.txt";
String answerFilePath = "answer.txt";
createTestFile(originalFilePath, "");
createTestFile(plagiarizedFilePath, "");
PaperChecker checker = new PaperChecker(originalFilePath, plagiarizedFilePath, answerFilePath);
double similarity = checker.calculateSimilarity();
assertEquals(0.0, similarity, 0.001); // 使用 delta 值处理浮点数比较
deleteTestFile(originalFilePath);
deleteTestFile(plagiarizedFilePath);
deleteTestFile(answerFilePath);
}
// 测试用例3:两个相同文件的相似度计算
// 目的:验证程序在输入两个完全相同的文件时能否正确处理。
// 预期结果:相似度值应为 1.0。
@Test
public void testCalculateSimilarity_identicalFiles() throws IOException {
// 准备测试数据(相同内容的文件)
String text = "这是一段相同的文本。";
String originalFilePath = "identical_original.txt";
String plagiarizedFilePath = "identical_plagiarized.txt";
String answerFilePath = "answer.txt";
createTestFile(originalFilePath, text);
createTestFile(plagiarizedFilePath, text);
PaperChecker checker = new PaperChecker(originalFilePath, plagiarizedFilePath, answerFilePath);
double similarity = checker.calculateSimilarity();
assertEquals(1.0, similarity, 0.001);
deleteTestFile(originalFilePath);
deleteTestFile(plagiarizedFilePath);
deleteTestFile(answerFilePath);
}
// 测试用例4:文件不存在时的异常处理
// 目的:验证程序在尝试读取不存在的文件时能否正确抛出 FileNotFoundException。
// 预期结果:应抛出 FileNotFoundException。
@Test
public void testReadFileContent_fileNotFound() {
PaperChecker checker = new PaperChecker("1.txt", "1.txt", "1.txt");
assertThrows(FileNotFoundException.class, () -> {
checker.readFileContent("nonexistent_file.txt");
});
}
// 测试用例5:构造函数文件路径为 null 时的异常处理
// 目的:验证构造函数能否正确处理文件路径为null的情况
// 预期结果: 应该抛出IllegalArgumentException
@Test
public void testCalculateSimilarity_nullFilePath() {
assertThrows(IllegalArgumentException.class, () -> {
new PaperChecker(null, "plag.txt", "answer.txt");
});
assertThrows(IllegalArgumentException.class, () -> {
new PaperChecker("orig.txt", null, "answer.txt");
});
assertThrows(IllegalArgumentException.class, () -> {
new PaperChecker("orig.txt", "plag.txt", null);
});
}
- 覆盖率
![]()
五.异常处理说明
| 异常类型 | 设计目标 | 单元测试样例 | 错误场景 |
|---|---|---|---|
| IllegalArgumentException | 检查传入的文件路径是否为 null 或空字符串。 | testCalculateSimilarity_nullFilePath | 文件路径为 null 或空字符串。 |
| FileNotFoundException | 检查要读取的文件是否存在。 | testReadFileContent_fileNotFound | 读取的文件不存在。 |
| IOException | 读取文件时发生其他 I/O 错误(如权限问题、磁盘错误等)。 | (需要模拟 I/O 错误,比较困难。可以在 readFileContent 的测试中,尝试创建一个不可读的文件,但这可能依赖于操作系统。) | 文件不可读,或者读取过程中发生其他 I/O 错误。 |
| NullPointerException | 检查readFileContent,preprocessText和bigramSplit的参数是否为空 | (可以在readFileContent,preprocessText和bigramSplit中添加测试用例) | 传入了空参数 |


浙公网安备 33010602011771号