如何截取Java StringBuilder字符串?

15

我有一个StringBuilder对象,需要进行修剪(即从任一端删除所有空格字符/u0020及以下的字符)。

我似乎找不到StringBuilder中可以执行此操作的方法。

这是我现在正在做的事情:

String trimmedStr = strBuilder.toString().trim();

这种方式可以得到预期的输出结果,但需要分配两个字符串而不是一个。是否有更高效的方法,在StringBuilder中修剪字符串时使用?

9个回答

29

不应使用 deleteCharAt 方法。

正如Boris所指出的那样,deleteCharAt方法每次都会复制数组。在Java 5中执行此操作的代码如下:

public AbstractStringBuilder deleteCharAt(int index) {
    if ((index < 0) || (index >= count))
        throw new StringIndexOutOfBoundsException(index);
    System.arraycopy(value, index+1, value, index, count-index-1);
    count--;
    return this;
}

当然,仅凭猜测是不足以选择一种优化方法而非另一种的,因此我决定对这个线程中的3种方法进行计时比较:原始方法、删除方法和子字符串方法。

以下是我测试原始方法的代码:

public static String trimOriginal(StringBuilder sb) {
    return sb.toString().trim();
}

删除方法:

public static String trimDelete(StringBuilder sb) {
    while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) {
        sb.deleteCharAt(0);
    }
    while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) {
        sb.deleteCharAt(sb.length() - 1);
    }
    return sb.toString();
}

然后是使用子字符串的方法:

public static String trimSubstring(StringBuilder sb) {
    int first, last;

    for (first=0; first<sb.length(); first++)
        if (!Character.isWhitespace(sb.charAt(first)))
            break;

    for (last=sb.length(); last>first; last--)
        if (!Character.isWhitespace(sb.charAt(last-1)))
            break;

    return sb.substring(first, last);
}

我进行了100次测试,每次生成一个带有一万个前导和尾随空格的百万字符StringBuffer。测试本身非常基础,但它可以很好地说明这些方法需要多长时间。

这里是计时三种方法的代码:

public static void main(String[] args) {

    long originalTime = 0;
    long deleteTime = 0;
    long substringTime = 0;

    for (int i=0; i<100; i++) {

        StringBuilder sb1 = new StringBuilder();
        StringBuilder sb2 = new StringBuilder();
        StringBuilder sb3 = new StringBuilder();

        for (int j=0; j<10000; j++) {
            sb1.append(" ");
            sb2.append(" ");
            sb3.append(" ");
        }
        for (int j=0; j<980000; j++) {
            sb1.append("a");
            sb2.append("a");
            sb3.append("a");
        }
        for (int j=0; j<10000; j++) {
            sb1.append(" ");
            sb2.append(" ");
            sb3.append(" ");
        }

        long timer1 = System.currentTimeMillis();
        trimOriginal(sb1);
        originalTime += System.currentTimeMillis() - timer1;

        long timer2 = System.currentTimeMillis();
        trimDelete(sb2);
        deleteTime += System.currentTimeMillis() - timer2;

        long timer3 = System.currentTimeMillis();
        trimSubstring(sb3);
        substringTime += System.currentTimeMillis() - timer3;
    }

    System.out.println("original:  " + originalTime + " ms");
    System.out.println("delete:    " + deleteTime + " ms");
    System.out.println("substring: " + substringTime + " ms");
}

我得到了以下输出:

original:  176 ms
delete:    179242 ms
substring: 154 ms

从我们可以看到,子字符串方法相对于原始的“两个字符串”方法提供了非常轻微的优化。然而,删除方法非常缓慢,应该避免使用。

因此,回答你的问题:按照你在问题中提出的方式修剪StringBuilder是没有问题的。子字符串方法提供的非常轻微的优化可能不足以证明额外的代码。


1
很好地阐述了我的评论,我自己没有时间。但是要小心Java中的微基准测试。JVM以许多方式优化运行代码,简单的微基准测试可能会误导(例如,请参见有缺陷的微基准测试剖析这个问题获取详细信息)。有像caliper这样的工具可以帮助进行微基准测试。 - Boris
在相关的问题中,还有一个答案,其中包含了一些相关文章和我之前用过的工具(但是我没有在我的第一条评论中找到它)。这些都是很好的阅读材料。 - Boris
嗨,鲍里斯,非常感谢你提供的信息。我正在阅读这篇文章,到目前为止,我发现它非常有用。 - Zaven Nahapetyan
使用子字符串会稍微提高速度,但我认为最大的改进在于内存方面,因为字符串不必像原始方法那样被复制两次。+1 - CodeFusionMobile
使用 delete(int, int) 方法来删除前导空格怎么样?只需计算起始位置,然后使用 setLength 方法即可。 - gvaish
显示剩余2条评论

3

我使用了Zaven的分析方法和StringBuilder的delete(start, end)方法,它比deleteCharAt(index)方法表现更好,但略逊于substring()方法。该方法还使用了数组复制,但在最坏的情况下只调用了两次数组复制。此外,这种方法避免了在同一StringBuilder对象上多次重复调用trim()时创建多个中间字符串实例

public class Main {

    public static String trimOriginal(StringBuilder sb) {
        return sb.toString().trim();
    }

    public static String trimDeleteRange(StringBuilder sb) {
        int first, last;

        for (first = 0; first < sb.length(); first++)
            if (!Character.isWhitespace(sb.charAt(first)))
                break;

        for (last = sb.length(); last > first; last--)
            if (!Character.isWhitespace(sb.charAt(last - 1)))
                break;

        if (first == last) {
            sb.delete(0, sb.length());
        } else {
           if (last < sb.length()) {
              sb.delete(last, sb.length());
           }
           if (first > 0) {
              sb.delete(0, first);
           }
        }
        return sb.toString();
    }


    public static String trimSubstring(StringBuilder sb) {
        int first, last;

        for (first = 0; first < sb.length(); first++)
            if (!Character.isWhitespace(sb.charAt(first)))
                break;

        for (last = sb.length(); last > first; last--)
            if (!Character.isWhitespace(sb.charAt(last - 1)))
                break;

        return sb.substring(first, last);
    }

    public static void main(String[] args) {
        runAnalysis(1000);
        runAnalysis(10000);
        runAnalysis(100000);
        runAnalysis(200000);
        runAnalysis(500000);
        runAnalysis(1000000);
    }

    private static void runAnalysis(int stringLength) {
        System.out.println("Main:runAnalysis(string-length=" + stringLength + ")");

        long originalTime = 0;
        long deleteTime = 0;
        long substringTime = 0;

        for (int i = 0; i < 200; i++) {

            StringBuilder temp = new StringBuilder();
            char[] options = {' ', ' ', ' ', ' ', 'a', 'b', 'c', 'd'};
            for (int j = 0; j < stringLength; j++) {
                temp.append(options[(int) ((Math.random() * 1000)) % options.length]);
            }
            String testStr = temp.toString();

            StringBuilder sb1 = new StringBuilder(testStr);
            StringBuilder sb2 = new StringBuilder(testStr);
            StringBuilder sb3 = new StringBuilder(testStr);

            long timer1 = System.currentTimeMillis();
            trimOriginal(sb1);
            originalTime += System.currentTimeMillis() - timer1;

            long timer2 = System.currentTimeMillis();
            trimDeleteRange(sb2);
            deleteTime += System.currentTimeMillis() - timer2;

            long timer3 = System.currentTimeMillis();
            trimSubstring(sb3);
            substringTime += System.currentTimeMillis() - timer3;
        }

        System.out.println("  original:     " + originalTime + " ms");
        System.out.println("  delete-range: " + deleteTime + " ms");
        System.out.println("  substring:    " + substringTime + " ms");
    }

}

输出:

Main:runAnalysis(string-length=1000)
  original:     0 ms
  delete-range: 4 ms
  substring:    0 ms
Main:runAnalysis(string-length=10000)
  original:     4 ms
  delete-range: 9 ms
  substring:    4 ms
Main:runAnalysis(string-length=100000)
  original:     22 ms
  delete-range: 33 ms
  substring:    43 ms
Main:runAnalysis(string-length=200000)
  original:     57 ms
  delete-range: 93 ms
  substring:    110 ms
Main:runAnalysis(string-length=500000)
  original:     266 ms
  delete-range: 220 ms
  substring:    191 ms
Main:runAnalysis(string-length=1000000)
  original:     479 ms
  delete-range: 467 ms
  substring:    426 ms

看到这个在迭代次数上进行基准测试会很有趣,而不是字符串长度。我想知道差异会有多大。 - CodeFusionMobile
应该很容易修改代码以添加对迭代的支持。我认为使用子字符串的结果仍然最佳,但从删除方法中获得的收益将来自于保存在垃圾回收中较少的对象,这在当前基准代码中没有进行测量。 - shams
@shams - 你为什么说这句话 - "StringBuilder的delete(start, end)方法比deleteCharAt(index)方法表现更好"? - Erran Morad

2
不用担心有两个字符串,这只是微小的优化。如果你真的发现了瓶颈,你可以进行几乎恒定时间的修剪 - 只需迭代前N个字符,直到它们是Character.isWhitespace(c)

1
我担心的原因是因为我正在Android上解析一个XML文件,所以这个操作将会重复执行数千次。这意味着我将会分配大约6000个字符串而不是3000个,这是一个非常好的改进。 - CodeFusionMobile
1
比这个优化更重要的是不使用StringBuilder的默认构造函数,而是给它一个足够的初始容量。在当前实现中,默认容量为16个字符,如果不够用,缓冲区将被一个更大的缓冲区替换。特别是对于长文本,这将带来比任何其他优化都更多的收益(因为缓冲区会为每行输入重新分配多次)。 - Axel
@Axel 说得好。我已经在 StringBuilder 构造函数中使用了 500 个字符的容量,这对我的应用程序来说是合适的。 - CodeFusionMobile

1

一开始我也有和你一样的问题,但是经过5分钟的思考后,我意识到实际上你不需要修剪StringBuffer!你只需要修剪你追加到StringBuffer中的字符串

如果你想修剪一个初始的StringBuffer,你可以这样做:

StringBuffer sb = new StringBuffer(initialStr.trim());

如果您想即时修剪StringBuffer,您可以在附加期间执行此操作:

Sb.append(addOnStr.trim());

1

只有你考虑到了将字符串构建器转换为“字符串”,然后进行“修剪”会创建两个不可变对象,需要进行垃圾回收,因此总分配量为:

  1. Stringbuilder 对象
  2. SB 对象的不可变字符串
  3. 已修剪的字符串的 1 个不可变对象。

因此,尽管修剪可能看起来更快,但在实际世界和具有加载内存方案的情况下,它实际上会更糟。


0
你得到了两个字符串,但我希望数据只分配一次。由于Java中的字符串是不可变的,我期望trim()实现会给你一个共享相同字符数据但具有不同起始和结束索引的对象。至少substr()方法就是这样做的。因此,任何你尝试优化这个过程肯定会产生相反的效果,因为你增加了不必要的开销。

使用调试器逐步执行trim()方法即可。


它返回一个新的字符串对象,该对象通过arraycopy复制字符:来自源代码: return new String(start,end - start + 1,value); - CodeFusionMobile
你在哪里看到了arraycopy?这是JDK 1.6.0中trim使用的String构造函数,缓冲区被重复使用而不进行复制: String(int offset, int count, char value[]) { this.value = value; this.offset = offset; this.count = count; } - Axel

0

我写了一些代码。它能正常运行,测试用例也在那里供你查看。如果没问题的话,请告诉我。

主要代码 -

public static StringBuilder trimStringBuilderSpaces(StringBuilder sb) {

    int len = sb.length();

    if (len > 0) {

            int start = 0;
            int end = 1;
            char space = ' ';
            int i = 0;

            // Remove spaces at start
            for (i = 0; i < len; i++) {
                if (sb.charAt(i) != space) {
                    break;
                }
            }

            end = i;
            //System.out.println("s = " + start + ", e = " + end);
            sb.delete(start, end);

            // Remove the ending spaces
            len = sb.length();

            if (len > 1) {

                for (i = len - 1; i > 0; i--) {
                    if (sb.charAt(i) != space) {
                        i = i + 1;
                        break;
                    }
                }

                start = i;
                end = len;// or len + any positive number !

                //System.out.println("s = " + start + ", e = " + end);
                sb.delete(start, end);

            }

    }

    return sb;
}

完整的代码和测试 -

package source;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;

public class StringBuilderTrim {

    public static void main(String[] args) {
        testCode();
    }

    public static void testCode() {

        StringBuilder s1 = new StringBuilder("");
        StringBuilder s2 = new StringBuilder(" ");
        StringBuilder s3 = new StringBuilder("  ");
        StringBuilder s4 = new StringBuilder(" 123");
        StringBuilder s5 = new StringBuilder("  123");
        StringBuilder s6 = new StringBuilder("1");
        StringBuilder s7 = new StringBuilder("123 ");
        StringBuilder s8 = new StringBuilder("123  ");
        StringBuilder s9 = new StringBuilder(" 123 ");
        StringBuilder s10 = new StringBuilder("  123  ");

        /*
         * Using a rough form of TDD here. Initially, one one test input
         * "test case" was added and rest were commented. Write no code for the
         * method being tested. So, the test will fail. Write just enough code
         * to make it pass. Then, enable the next test. Repeat !!!
         */
        ArrayList<StringBuilder> ins = new ArrayList<StringBuilder>();
        ins.add(s1);
        ins.add(s2);
        ins.add(s3);
        ins.add(s4);
        ins.add(s5);
        ins.add(s6);
        ins.add(s7);
        ins.add(s8);
        ins.add(s9);
        ins.add(s10);

        // Run test
        for (StringBuilder sb : ins) {
            System.out
                    .println("\n\n---------------------------------------------");
            String expected = sb.toString().trim();
            String result = trimStringBuilderSpaces(sb).toString();
            System.out.println("In [" + sb + "]" + ", Expected [" + expected
                    + "]" + ", Out [" + result + "]");
            if (result.equals(expected)) {
                System.out.println("Success!");
            } else {
                System.out.println("FAILED!");
            }
            System.out.println("---------------------------------------------");
        }

    }

    public static StringBuilder trimStringBuilderSpaces(StringBuilder inputSb) {

        StringBuilder sb = new StringBuilder(inputSb);
        int len = sb.length();

        if (len > 0) {

            try {

                int start = 0;
                int end = 1;
                char space = ' ';
                int i = 0;

                // Remove spaces at start
                for (i = 0; i < len; i++) {
                    if (sb.charAt(i) != space) {
                        break;
                    }
                }

                end = i;
                //System.out.println("s = " + start + ", e = " + end);
                sb.delete(start, end);

                // Remove the ending spaces
                len = sb.length();

                if (len > 1) {

                    for (i = len - 1; i > 0; i--) {
                        if (sb.charAt(i) != space) {
                            i = i + 1;
                            break;
                        }
                    }

                    start = i;
                    end = len;// or len + any positive number !

                    //System.out.println("s = " + start + ", e = " + end);
                    sb.delete(start, end);

                }

            } catch (Exception ex) {

                StringWriter sw = new StringWriter();
                PrintWriter pw = new PrintWriter(sw);
                ex.printStackTrace(pw);
                sw.toString(); // stack trace as a string

                sb = new StringBuilder("\nNo Out due to error:\n" + "\n" + sw);
                return sb;
            }

        }

        return sb;
    }
}

0
strBuilder.replace(0,strBuilder.length(),strBuilder.toString().trim());

3
尽管这段代码片段可能解决了问题,但"包括解释"(//meta.stackoverflow.com/questions/114762/explaining-entirely-code-based-answers)确实有助于提高您的帖子质量。请记住,您正在为未来的读者回答问题,而这些人可能不知道您建议代码的原因。同时,请尽量不要在代码中添加过多的解释性注释,这会降低代码和解释的可读性! - Filnor

0
由于 deleteCharAt() 每次都会复制数组,因此我采用了下面的代码,在 StringBuilder 同时具有前导和尾随空格的最坏情况下,该代码将复制数组两次。以下代码将确保对象引用保持不变,并且我们不会创建新对象。
    public static void trimStringBuilder(StringBuilder builder) {
        int len = builder.length();
        int start = 0;
        // Remove whitespace from start
        while (start < len && builder.charAt(start) == ' ') {
            start++;
        }
        if (start > 0) {
            builder.delete(0, start);
        }
    
        len = builder.length();
        int end = len;
        // Remove whitespace from end
        while (end > 0 && builder.charAt(end - 1) == ' ') {
            end--;
        }
        if (end < len) {
            builder.delete(end, len);
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接