本文目录导读:

- 方案一:基于LCS算法的文本行比对(最核心、最常用)
- 方案二:处理大文件的优化(行级流式处理)
- 方案三:检测“修改”而非仅“新增/删除”(更智能的合同比对)
- 方案四:对比Word文档(.docx)格式的合同(生产环境重点)
- 方案五:直接使用成熟的Java Diff库(强烈推荐)
- 完整项目建议架构
- 总结建议
这是一个专业性很强的Java应用需求,合同比对的核心在于文本差异分析,而非简单的字符串相等判断。
在实际的合同比对场景中(如法务、金融),通常关注的不仅是“改了哪里”,还包括新增、删除、修改以及格式变化。
下面我会从最经典的 Diff算法(最长公共子序列,LCS) 实现、到处理大文件的优化方案、以及如何结合文档格式(如Word)三个层面,给出一个完整的Java实现方案。
基于LCS算法的文本行比对(最核心、最常用)
这是最标准的做法,将合同按行分割,然后找出两段文本的“最长公共子序列”,未被包含的部分即为差异。
核心算法:计算Diff矩阵(LCS)
import java.util.*;
public class DiffUtils {
/**
* 计算两个文本数组的最长公共子序列(LCS)路径
* @param original 原始文本行列表
* @param revised 修改后文本行列表
* @return 差异操作列表
*/
public static List<DiffEntry> computeDiff(List<String> original, List<String> revised) {
int m = original.size();
int n = revised.size();
// 1. 构建LCS长度矩阵
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (original.get(i - 1).equals(revised.get(j - 1))) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
// 2. 回溯,生成Diff操作
List<DiffEntry> result = new ArrayList<>();
int i = m, j = n;
// 使用栈来反转顺序(因为回溯是从后往前)
Deque<DiffEntry> stack = new ArrayDeque<>();
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && original.get(i - 1).equals(revised.get(j - 1))) {
// 相等:保留
stack.push(new DiffEntry(DiffType.EQUAL, original.get(i - 1)));
i--;
j--;
} else if (j > 0 && (i == 0 || dp[i][j - 1] >= dp[i - 1][j])) {
// 插入:新增的行
stack.push(new DiffEntry(DiffType.INSERT, revised.get(j - 1)));
j--;
} else if (i > 0) {
// 删除:移除的行
stack.push(new DiffEntry(DiffType.DELETE, original.get(i - 1)));
i--;
}
}
// 从栈中弹出,得到正确顺序
while (!stack.isEmpty()) {
result.add(stack.pop());
}
return result;
}
// 差异类型枚举
public enum DiffType {
EQUAL, // 相同
INSERT, // 新增
DELETE // 删除
}
// 差异条目
public static class DiffEntry {
private DiffType type;
private String line;
public DiffEntry(DiffType type, String line) {
this.type = type;
this.line = line;
}
public DiffType getType() { return type; }
public String getLine() { return line; }
@Override
public String toString() {
return (type == DiffType.INSERT ? "+ " :
type == DiffType.DELETE ? "- " : " ") + line;
}
}
}
使用示例 & 可视化输出
public class ContractComparator {
public static void main(String[] args) {
String text1 = """
合同第一条:关于货款
甲方应于2024年1月1日前支付。
逾期未付,按日收取万分之五的违约金。
""";
String text2 = """
合同第一条:关于货款
甲方应于2024年3月1日前支付。
逾期未付,按日收取万分之三的违约金。
若逾期超过30天,甲方可解除合同。
""";
// 1. 按行分割
List<String> originalLines = Arrays.asList(text1.split("\n"));
List<String> revisedLines = Arrays.asList(text2.split("\n"));
// 2. 执行比对
List<DiffUtils.DiffEntry> diffs = DiffUtils.computeDiff(originalLines, revisedLines);
// 3. 输出比对结果(类似Git diff风格)
System.out.println("=== 合同比对结果 ===");
diffs.forEach(entry -> {
switch (entry.getType()) {
case INSERT:
System.out.println("\033[32m+ " + entry.getLine() + "\033[0m"); // 绿色
break;
case DELETE:
System.out.println("\033[31m- " + entry.getLine() + "\033[0m"); // 红色
break;
case EQUAL:
System.out.println(" " + entry.getLine());
break;
}
});
}
}
处理大文件的优化(行级流式处理)
如果合同非常大(如几十万行),上述二维数组dp[m+1][n+1]会耗尽内存,可以采用Myers差分算法或滑动窗口优化。
这里提供一个改进版:只保留两行DP数组(空间复杂度 O(n)),适合行数极多但差异不大的情况。
public static List<DiffEntry> computeDiffOptimized(List<String> original, List<String> revised) {
int m = original.size();
int n = revised.size();
// 只能得到长度,无法直接回溯路径
// 实际生产环境中通常使用 "分治策略" + "线性空间LCS"
// 这里简化:对于大文件,建议直接调用成熟的库(见下文)
throw new UnsupportedOperationException("大规模文件建议使用google-diff-match-patch或git diff");
}
检测“修改”而非仅“新增/删除”(更智能的合同比对)
合同比对中,“万分之五” 被改为 “万分之三” ,更合理的展示应该是修改操作,而不是先删一行再加一行。
实现方式:在回溯完成后,对相邻的 DELETE + INSERT 进行合并。
public static List<DiffEntry> mergeModifications(List<DiffEntry> diffs) {
List<DiffEntry> merged = new ArrayList<>();
for (int i = 0; i < diffs.size(); i++) {
DiffEntry current = diffs.get(i);
// 如果当前是DELETE,且下一个是INSERT,合并为MODIFY
if (current.getType() == DiffType.DELETE
&& i + 1 < diffs.size()
&& diffs.get(i + 1).getType() == DiffType.INSERT) {
DiffEntry next = diffs.get(i + 1);
merged.add(new DiffEntry(DiffType.MODIFY,
"旧: " + current.getLine() + " | 新: " + next.getLine()));
i++; // 跳过下一个INSERT
} else {
merged.add(current);
}
}
return merged;
}
// 别忘了在DiffType枚举中添加 MODIFY 类型
对比Word文档(.docx)格式的合同(生产环境重点)
合同通常以Word格式存在,直接对比文本会丢失格式信息。
使用 Apache POI 提取Word文本 + Diff
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
import org.apache.poi.xwpf.usermodel.*;
public static String extractTextFromDocx(String filePath) throws Exception {
StringBuilder sb = new StringBuilder();
try (XWPFDocument doc = new XWPFDocument(new FileInputStream(filePath))) {
for (XWPFParagraph para : doc.getParagraphs()) {
sb.append(para.getText()).append("\n");
}
// 还可以提取表格中的文本
for (XWPFTable table : doc.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
sb.append(cell.getText()).append("\t");
}
sb.append("\n");
}
}
}
return sb.toString();
}
直接使用成熟的Java Diff库(强烈推荐)
手写LCS算法适合学习,但生产环境建议使用以下高性能、高稳定性的库:
google-diff-match-patch(Google出品,最强)
<dependency>
<groupId>org.bitbucket.cowwoc</groupId>
<artifactId>diff-match-patch</artifactId>
<version>1.2</version>
</dependency>
核心代码(支持字符级、词级、行级 + 语义清理):
import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch; DiffMatchPatch dmp = new DiffMatchPatch(); LinkedList<DiffMatchPatch.Diff> diffs = dmp.diffMain(text1, text2); dmp.diffCleanupSemantic(diffs); // 语义优化 String html = dmp.diffPrettyHtml(diffs); System.out.println(html); // 可以直接展示在网页上
java-diff-utils(轻量级,易用)
<dependency>
<groupId>io.github.java-diff-utils</groupId>
<artifactId>java-diff-utils</artifactId>
<version>4.12</version>
</dependency>
import com.github.difflib.text.DiffRowGenerator;
DiffRowGenerator generator = DiffRowGenerator.create()
.showInlineDiffs(true) // 显示行内差异(如万->三)
.mergeOriginalRevised(true)
.build();
List<DiffRow> rows = generator.generateDiffRows(
Arrays.asList(text1.split("\n")),
Arrays.asList(text2.split("\n"))
);
rows.forEach(row -> {
System.out.println(row.getOldLine() + " | " + row.getNewLine());
});
完整项目建议架构
src/
├── service/
│ ├── ContractComparator.java // 调用Diff入口
│ ├── DiffAlgorithm.java // LCS或Myers实现
│ └── DiffDisplayFormatter.java // 格式化输出(HTML/控制台/JSON)
├── model/
│ └── DiffEntry.java // 差异实体
├── parser/
│ ├── TextParser.java // 文本、PDF、Word解析
│ └── DocxParser.java // Apache POI解析docx
└── Application.java // 主程序
总结建议
- 入门/学习:使用代码中的 LCS算法 + 合并修改。
- 生产环境:直接使用
java-diff-utils或google-diff-match-patch,它们的性能、语义处理、边缘情况处理都比手写的好很多。 - 特殊格式:如果需要保留Word格式对比(高亮、批注),可能需要更复杂的解决方案(如基于XML差分),推荐使用商业库或
Apache POI+difflib组合。
希望这个案例能帮助你快速实现合同比对功能!