虽然StringBuilder.replace() 与String.replace() 相比有了巨大的改进,但距离最佳状态还有很远。
StringBuilder.replace() 的问题在于,如果替换部分的长度与可替换部分的长度不同(适用于我们的案例),则可能必须分配更大的内部 char 数组,并且必须复制内容,并且然后会发生替换(这也涉及复制)。
想象一下:您有一个包含 10.000 个字符的文本。如果要将在位置1(第二个字符)找到的"XY" 子字符串替换为"ABC",则实现必须重新分配至少大1 的char 缓冲区,必须将旧内容复制到新数组,它必须将 9.997 个字符(从位置 3 开始)向右复制 1 以将 "ABC" 放入 "XY" 的位置,最后将 "ABC" 的字符复制到起始位置1。每次更换都必须这样做!这很慢。
更快的解决方案:即时构建输出
我们可以构建输出on-the-fly:不包含可替换文本的部分可以简单地附加到输出中,如果我们找到可替换的片段,我们会附加替换其中。从理论上讲,仅循环输入一次 就足以生成输出。听起来很简单,实现起来并不难。
实施:
我们将使用预加载可替换替换字符串映射的Map:
Map<String, String> map = new HashMap<>();
map.put("<h1>", "<big><big><big><b>");
map.put("</h1>", "</b></big></big></big>");
map.put("<h2>", "<big><big>");
map.put("</h2>", "</big></big>");
map.put("<h3>", "<big>");
map.put("</h3>", "</big>");
map.put("<h4>", "<b>");
map.put("</h4>", "</b>");
map.put("<h5>", "<small><b>");
map.put("</h5>", "</b></small>");
map.put("<h6>", "<small>");
map.put("</h6>", "</small>");
使用这个,这里是替换代码:(代码后有更多解释)
public static String replaceTags(String src, Map<String, String> map) {
StringBuilder sb = new StringBuilder(src.length() + src.length() / 2);
for (int pos = 0;;) {
int ltIdx = src.indexOf('<', pos);
if (ltIdx < 0) {
// No more '<', we're done:
sb.append(src, pos, src.length());
return sb.toString();
}
sb.append(src, pos, ltIdx); // Copy chars before '<'
// Check if our hit is replaceable:
boolean mismatch = true;
for (Entry<String, String> e : map.entrySet()) {
String key = e.getKey();
if (src.regionMatches(ltIdx, key, 0, key.length())) {
// Match, append the replacement:
sb.append(e.getValue());
pos = ltIdx + key.length();
mismatch = false;
break;
}
}
if (mismatch) {
sb.append('<');
pos = ltIdx + 1;
}
}
}
测试:
String in = "Yo<h1>TITLE</h1><h3>Hi!</h3>Nice day.<h6>Hi back!</h6>End";
System.out.println(in);
System.out.println(replaceTags(in, map));
输出:(包装以避免滚动条)
Yo<h1>TITLE</h1><h3>Hi!</h3>Nice day.<h6>Hi back!</h6>End
Yo<big><big><big><b>TITLE</b></big></big></big><big>Hi!</big>Nice day.
<small>Hi back!</small>End
此解决方案比使用正则表达式要快,因为这涉及很多开销,例如编译Pattern、创建Matcher 等,而正则表达式也更通用。它还在引擎盖下创建了许多临时对象,这些临时对象在替换后被丢弃。在这里,我只使用了StringBuilder(加上其内部的char 数组),并且代码只对输入String 进行了一次迭代。此外,此解决方案比使用此答案顶部的 StringBuilder.replace() 快得多。
注释及说明
我在replaceTags() 方法中初始化了StringBuilder,如下所示:
StringBuilder sb = new StringBuilder(src.length() + src.length() / 2);
所以基本上我创建的初始容量是原始String 长度的 150%。这是因为我们的替换比可替换的文本长,所以如果发生替换,输出显然会比输入长。为StringBuilder 提供更大的初始容量将根本不会导致内部char[] 重新分配(当然,所需的初始容量取决于可替换-替换对及其在输入中的频率/出现率,但这个 +50% 是良好的上估计)。
我还利用了所有可替换字符串都以 '<' 字符开头的事实,因此找到下一个潜在的可替换位置变得非常快:
int ltIdx = src.indexOf('<', pos);
这只是一个简单的循环和char 在String 中的比较,并且由于它总是从pos 开始搜索(而不是从输入的开头),所以总体而言,代码只迭代输入String一次。
最后要判断一个可替换的String 是否确实出现在潜在位置,我们使用String.regionMatches() 方法来检查可替换的stings,它也非常快,因为它只是比较char 中的值一个循环并在第一个不匹配的字符处返回。
还有一个优点:
问题没有提到它,但我们的输入是一个 HTML 文档。 HTML 标签不区分大小写,这意味着输入可能包含<H1> 而不是<h1>。
对于这个算法,这不是问题。 String 类中的 regionMatches() 有一个重载,supports case-insensitive comparison:
boolean regionMatches(boolean ignoreCase, int toffset, String other,
int ooffset, int len);
所以如果我们想修改我们的算法来查找和替换相同但使用不同字母大小写的输入标签,我们只需要修改这一行:
if (src.regionMatches(true, ltIdx, key, 0, key.length())) {
使用此修改后的代码,可替换标签变得不区分大小写:
Yo<H1>TITLE</H1><h3>Hi!</h3>Nice day.<H6>Hi back!</H6>End
Yo<big><big><big><b>TITLE</b></big></big></big><big>Hi!</big>Nice day.
<small>Hi back!</small>End