1. 概述
本文将介绍如何使用 Java 找出两个字符串之间的差异。
我们不会从零实现算法,而是基于两个成熟的第三方库来对比分析它们的实现方式和适用场景。目标是帮你根据实际需求选择更合适的工具,避免踩坑。
2. 问题背景
假设我们有这样两个字符串:
text1 = "ABCDELMN"
text2 = "ABCFGLMN"
我们的目标是:找出它们之间的差异。
如果不用自己写 Diff 算法(那可是大工程),目前主流的现成方案主要有两个:
✅ Google 的 diff-match-patch
功能强大,适合复杂文本对比,比如同步、合并、高亮差异等场景。
✅ Apache Commons Lang 的 StringUtils
轻量简单,适合快速判断从哪开始不一样。
下面我们分别来看它们的用法和输出结果。
3. diff-match-patch:细粒度差异分析
⚠️ 注意:原版 Google 的
diff-match-patch
没有发布到 Maven Central,我们使用社区维护的 Java 适配版本。
添加依赖
<dependency>
<groupId>org.bitbucket.cowwoc</groupId>
<artifactId>diff-match-patch</artifactId>
<version>1.2</version>
</dependency>
使用示例
String text1 = "ABCDELMN";
String text2 = "ABCFGLMN";
DiffMatchPatch dmp = new DiffMatchPatch();
LinkedList<Diff> diff = dmp.diffMain(text1, text2, false);
执行后,diff
的输出如下:
[Diff(EQUAL,"ABC"), Diff(DELETE,"DE"), Diff(INSERT,"FG"), Diff(EQUAL,"LMN")]
输出说明
- 每个
Diff
对象包含两个部分:操作类型 和 对应文本片段 - 操作类型有三种:
EQUAL
:两字符串共有的部分DELETE
:在text1
中存在但在text2
中被删除的部分INSERT
:在text2
中新增的部分
反向比较
如果反过来比较 text2
和 text1
:
dmp.diffMain(text2, text1, false);
输出为:
[Diff(EQUAL,"ABC"), Diff(DELETE,"FG"), Diff(INSERT,"DE"), Diff(EQUAL,"LMN")]
可以看到,DELETE
和 INSERT
的内容互换了 —— 这正是 diff 的对称性体现。
适用场景
- 文本编辑器的版本对比
- API 响应内容变更高亮
- 数据同步与合并逻辑
✅ 优势:输出结构化,信息完整
❌ 缺点:性能开销大,不适合高频调用
4. StringUtils:简单粗暴的差异提取
来自 Apache Commons Lang 的 StringUtils
提供了一个极简的方案。
添加依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
使用方式
StringUtils.difference(text1, text2)
输出结果是一个字符串:
"FGLMN"
表示从第4个字符开始,text2
中不同的后半部分。
反过来:
StringUtils.difference(text2, text1)
输出:
"DELMN"
更进一步:获取差异起始索引
如果你不仅想知道“差成啥样”,还想定位“从哪开始不一样”,可以用:
int index = StringUtils.indexOfDifference(text1, text2);
// 返回 3(索引从0开始)
然后你可以手动截取:
String commonPrefix = text1.substring(0, index); // "ABC"
String diffSuffix = text1.substring(index); // "DELMN"
适用场景
- 日志比对快速定位
- 配置项变更提醒
- 单元测试中简单断言
✅ 优势:速度快,API 简洁
❌ 缺点:只返回后缀差异,无法区分插入/删除
5. 性能对比
我们做了一组基准测试(Benchmark),模拟真实场景下的性能表现。
测试数据生成
- 生成 10,000 个字符串
- 每个字符串结构:前10位固定 + 后20位随机字母
测试代码
@Benchmark
public int diffMatchPatch() {
for (int i = 0; i < inputs.size() - 1; i++) {
diffMatchPatch.diffMain(inputs.get(i), inputs.get(i + 1), false);
}
return inputs.size();
}
@Benchmark
public int stringUtils() {
for (int i = 0; i < inputs.size() - 1; i++) {
StringUtils.difference(inputs.get(i), inputs.get(i + 1));
}
return inputs.size();
}
测试结果(AVG 时间)
Benchmark | Score | Units |
---|---|---|
diffMatchPatch |
130.559 ± 1.501 | ms/op |
stringUtils |
0.211 ± 0.003 | ms/op |
✅
StringUtils
比diff-match-patch
快 600倍以上
结论
- 如果你只是想快速知道“从哪开始不一样”,用
StringUtils.difference()
就够了 - 如果你需要知道“哪里删了、哪里加了”,才考虑上
diff-match-patch
6. 总结
特性 | diff-match-patch | StringUtils |
---|---|---|
输出粒度 | 细(区分 INSERT/DELETE/EQUAL) | 粗(仅返回差异后缀) |
返回类型 | List<Diff> 结构化数据 |
String 简单字符串 |
性能 | ❌ 慢(~130ms) | ✅ 极快(~0.2ms) |
适用场景 | 文本对比、合并、同步 | 快速差异定位、日志分析 |
📌 一句话建议:
日常小功能优先选
StringUtils
,别为了“看起来专业”而引入重型库;真要搞文本 diff 高亮或合并,再上diff-match-patch
。
完整示例代码已托管至 GitHub:https://github.com/example-java/string-diff-demo