高效地将最后几个字符添加到StringBuilder中

3
注意:本问题涉及 Java >= 9 引入的"紧凑字符串"
假设我正在向一个 StringBuilder 中追加未知数量的字符串(或字符),并在某个时刻确定我正在追加最后一个字符串。如何高效地完成这项任务?
背景:如果字符串构建器的容量不够大,它将始终将其增加到 max(oldCap + str.lenght(), oldCap * 2 + 2)。因此,如果您运气不佳,容量不足以容纳最后一个字符串,它将不必要地使容量翻倍,例如:
StringBuilder sb = new StringBuilder(4000);
sb.append("aaa..."); // 4000 * "a"
// Last string:
sb.append("b"); // Unnecessarily increases capacity from 4000 to 8002
return sb.toString();

StringBuilder提供了capacity()length()getChars(...)方法,但是手动创建一个char[],然后创建一个字符串将会效率低下,因为:

  • 由于“紧凑字符串”,字符串生成器必须将其字节转换为字符
  • 调用其中一个String构造函数时,字符必须再次被压缩为字节

另一种选择是检查capacity(),如果需要则创建一个new StringBuilder(sb.length() + str.length()),然后附加sbstr

StringBuilder sb = new StringBuilder(4000);
sb.append("aaa..."); // 4000 * "a"

String str = "b";
if (sb.capacity() - sb.length() < str.length()) {
    return new StringBuilder(sb.length() + str.length())
        .append(sb)
        .append(str)
        .toString();
}
else {
    return sb.append(str).toString();
}

唯一的缺点是,如果现有的字符串生成器或新字符串不是Latin 1(每个字符2字节),那么新创建的字符串生成器必须从每个字符1字节(Latin 1)“膨胀”到每个字符2字节。

1
由于专家建议“尽可能使用+而不是StringBuilder”,因此请使用return sb.toString() + str;(请参见“今天的教训”此处(通过此文章)). 我不认识这些专家,所以您可能需要调查他们的可信度。 - vanOekel
好的观点,Aleksey Shipilëv 在 这个演示文稿 中也有描述。在我的情况下,我实际上正在处理 char[],但将它们转换为字符串,然后使用字符串连接可能仍然更有效率。 - Marcono1234
1
@vanOekel 他们说的是一半正确的,它并不是直接编译成 StringBuilder,但五种策略中有四种仍然在幕后使用它。 - Eugene
1
@vanOekel 当然,“在可能的情况下”是错误的,当您在循环中连接字符串时 - 没有任何变化,直接使用 StringBuilder 更加高效。更多信息 - Eugene
1
@vanOekel 但是,可以使用return sb + str;而不强制创建另一个中间的String实例。 - Holger
1个回答

1
您在我看来是在描述两个不同的问题,但它们都不是“实际”的问题。
首先,StringBuilder 分配了太多空间 - 在实践中很少(如果有的话)会出现这种问题。想想任何一个 List/Set/Map - 它们都做同样的事情,可能会分配太多的空间,但当您删除一个元素时,它们不会缩小其内部存储。它们确实有一个方法可以解决这个问题;但是 StringBuilder 也有:
 trimToSize

由于“紧凑字符串”,字符串构建器必须将其字节转换为字符。StringBuilder通过其继承的AbstractStringBuilder中的coder字段知道它正在存储什么。对于紧凑字符串,String现在使用byte[]保存其数据(它也有一个编码器),因此我不明白从byte[]到char[]的转换应该发生在哪里。StringBuilder :: toString定义如下:
public String toString() {
    // Create a copy, don't share the array
    return isLatin1() ? StringLatin1.newString(value, 0, count)
                      : StringUTF16.newString(value, 0, count);
}

注意isLatin1的检查 - StringBuilder 内部知道它拥有的数据类型;因此在可能的情况下不需要转换。
我假设你的意思是:
当调用 String 构造函数之一时,字符必须再次压缩为字节。
char [] some = ...
String s = new String(some);

我不知道为什么你在这里再次使用again,但可能是我漏掉了什么。只需注意,从char[]byte[]的转换确实必须发生,但这很容易做到(最后8位必须为空),并且只要单个char不符合前提条件,整个转换就会失败。因此,你要么将所有字符存储在LATIN1中,要么不存储。


这两个引号是指前面的句子:“然而手动创建char[],然后再创建字符串会很低效,因为”。 - Marcono1234
@Marcono1234,你能否提供一个你所想的例子吗?我好像没明白,可能是我的错误? - Eugene
@Marcono1234,我觉得这种只关注最后一个元素的方法有点不理性。如果不必要的大容量增加操作发生在倒数第二个元素或倒数第三个元素怎么办? - Holger
@Holger,如果您不知道后面还有多少元素,那么您无法对此做任何事情。但是对于最后一个元素,您应该知道它是最后一个并且应该能够进行优化。 - Marcono1234
@Holger,但如果这个角落情况的要求是 if (sb.capacity() - sb.length() < str.length())(所有简单的getter),并且它成功了999次,那么JVM也应该能够进行优化,不是吗?我也不是在问4K与8K数组之间的差异的情况,而是例如用户可能会读取Base64二进制数据(作为字符串而不是使用Reader),这可能会变得非常大。 - Marcono1234
显示剩余3条评论

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