连续的String.replace的替代方案

时间:2014-11-04 12:34:09

标签: java string replace

我想替换String输入中的一些字符串:

string=string.replace("<h1>","<big><big><big><b>");
string=string.replace("</h1>","</b></big></big></big>");
string=string.replace("<h2>","<big><big>");
string=string.replace("</h2>","</big></big>");
string=string.replace("<h3>","<big>");
string=string.replace("</h3>","</big>");
string=string.replace("<h4>","<b>");
string=string.replace("</h4>","</b>");
string=string.replace("<h5>","<small><b>");
string=string.replace("</h5>","</b><small>");
string=string.replace("<h6>","<small>");
string=string.replace("</h6>","</small>");

正如你所看到的那样,这种方法并不是最好的,因为每次我都要搜索要替换的部分等,并且字符串是不可变的...而且输入也很大,这意味着一些性能问题是考虑。

有没有更好的方法来降低此代码的复杂性?

7 个答案:

答案 0 :(得分:37)

虽然StringBuilder.replace()String.replace()相比是一个巨大的改进,但它仍然很远不是最佳的。

StringBuilder.replace()的问题在于,如果替换的长度与可替换部分的长度不同(适用于我们的情况),则可能必须分配更大的内部char数组,并且内容必须为被复制,然后将发生替换(这也涉及复制)。

想象一下:你有一个包含10.000个字符的文本。如果要将位置"XY"(第2个字符)中找到的1子字符串替换为"ABC",则实现必须重新分配char缓冲区,该缓冲区至少大1 ,必须将旧内容复制到新数组,并且必须将9.997个字符(从位置3开始)向右复制1以使"ABC"适合"XY"的位置,最后将"ABC"的字符复制到起始位置1。每次更换都必须这样做!这很慢。

更快的解决方案:即时构建输出

我们可以在运行中构建输出 :不包含可替换文本的部分可以简单地附加到输出中,如果我们找到可替换的片段,我们会附加替换它的。从理论上讲,只需一次就可以在输入上循环以生成输出。听起来很简单,实现起来并不难。

<强>实施

我们将使用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等等,而regexp也更通用。它还会在引擎盖下创建许多临时物体,这些物体在更换后会被丢弃。在这里,我只使用StringBuilder(加上char数组),代码只在输入String上迭代一次。此解决方案也比使用此答案顶部详述的StringBuilder.replace()快得多。

注释和解释

我在StringBuilder方法中初始化了replaceTags(),如下所示:

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()方法检查可更换的叮咬,这也是炽热的,因为它所做的只是比较{ {1}}循环中的值,并返回第一个不匹配的字符。

加上:

问题没有提及,但我们的输入是一个HTML文档。 HTML标记不区分大小写,这意味着输入可能包含char而不是<H1> 对于这个算法,这不是问题。 <h1>类中的regionMatches()重载supports case-insensitive comparison

String

因此,如果我们想要修改我们的算法以查找和替换相同但使用不同字母大小写的输入标签,我们只需要修改这一行:

boolean regionMatches(boolean ignoreCase, int toffset, String other,
                          int ooffset, int len);

使用此修改后的代码,可替换标记变得不区分大小写:

if (src.regionMatches(true, ltIdx, key, 0, key.length())) {

答案 1 :(得分:13)

对于表现 - 使用StringBuilder。 为方便起见,您可以使用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>");
...
StringBuilder builder = new StringBuilder(yourString);
for (String key : map.keySet()) {
    replaceAll(builder, key, map.get(key));
}

...要替换StringBuilder中的所有出现,您可以在此处查看: Replace all occurrences of a String using StringBuilder?

public static void replaceAll(StringBuilder builder, String from, String to)
{
    int index = builder.indexOf(from);
    while (index != -1)
    {
        builder.replace(index, index + from.length(), to);
        index += to.length(); // Move to the end of the replacement
        index = builder.indexOf(from, index);
    }
}

答案 2 :(得分:8)

不幸的是StringBuilder没有提供replace(string,string)方法,因此您可能需要考虑将PatternMatcherStringBuffer结合使用:

String input = ...;
StringBuffer sb = new StringBuffer();

Pattern p = Pattern.compile("</?(h1|h2|...)>");
Matcher m = p.matcher( input );
while( m.find() )
{
  String match = m.group();
  String replacement = ...; //get replacement for match, e.g. by lookup in a map

  m.appendReplacement( sb, replacement );
}
m.appendTail( sb );

您可以使用StringBuilder执行类似操作,但在这种情况下,您必须自己实施appendReplacement等。

至于表达式,你也可以尝试匹配任何 html标签(虽然这可能会导致问题,因为正则表达式和任意html不太合适)并且当查找没有时任何结果你只需用自己替换匹配。

答案 3 :(得分:5)

您提供的特定示例似乎是HTML或XHTML。尝试使用正则表达式编辑HTML或XML很容易出问题。对于您似乎感兴趣的编辑类型,您应该考虑使用XSLT。另一种可能性是使用流式XML解析器SAX,并让您的后端即时编写已编辑的输出。如果文本实际上是HTML,您可能更好地使用容忍的HTML解析器(例如JSoup)来构建文档的解析表示(如DOM),并在输出之前对其进行操作。

答案 4 :(得分:4)

StringBuilder由char数组支持。因此,与 String 实例不同,它是 mutable 。因此,您可以在indexOf()上致电replace()StringBuilder

答案 5 :(得分:3)

我会做这样的事情

    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < str.length(); i++) {
        if (tagEquals(str, i, "h1")) {
            sb.append("<big><big><big><b>");
            i += 2;
        } else (tagEquals(s, i, "/h1")) { 
            ...
        } else {
            sb.append(str.charAt(i));
        }
    }

tagEquals是一个检查标签名称

的函数

答案 6 :(得分:3)

使用Apache Commons StringUtils.replaceEach

String[] searches =     new String[]{"<h1>",                "</h1>",                  "<h2>", ...};
String[] replacements = new String[]("<big><big><big><b>",  "</b></big></big></big>", "<big><big>" ...};
string = StringUtils.replaceEach(string, searches, replacements);