StringBuffer,StringBuilder以及String

今天在网上闲逛,看见 @姚冬 的一个回答

他提到的问题也很有深度,然后思考了下,想评论来着。然而评论区太小,写不下,所以单独写在这儿。

基本上可以当作快问快答来读…

为什么java中的string不以\0结尾?

  • \0结尾在很大程度上要求程序员写规范的代码,如果写出了不规范的代码,那么很容易就内存越界了。
  • 另外,string的内部存储是char[],而为了内存安全,java数组本来就有一个length属性,这时以\0结尾就是一个多余的设计了。
  • String的内部存储也只能是char[]了,如果是其他的方式,比如通过native内部放一个c风格的数组,那么java代码中的char[]和string的转换就要很多内存拷贝操作了。
  • 而C语言设计成\0结尾,是为了减少抽象层,让C语言更加贴近硬件

(在语言设计中,)字符串的长度放哪里,放到起始指针的位置,还是起始指针的前面 ?

  • Java中,String的length也就是数组的length,JLS也只是说明了arraylength字节码,没有规定如何实现
  • 不过Hot Spot的实现是,先元数据,再长度,再具体的内容(比如char[])

如果放前面,那么字符串起始指针和内存块起始不一致怎么解决

Java不存在这个问题,我觉得。元数据和length字段都在实际数组之前呢。Java中,访问任何对象之前都要再多一次跳转,跳过元数据(和length)。

字符串拼接的时候把源串复制到目标串结尾,那么目标串剩余内存不够怎么办,重新分配要多一次赋值,频繁拼接性能有问题怎么办

  • 这个问题比较核心,Java的String不可变设计导致了只能单独复制一份来new String。
  • 这个问题,编译器、JVM很难优化,你把单个的字符串拼接操作优化成StringBuilder的append,但是也扛不住我在for循环中频繁append啊。而且每次new一个StringBuilder也是很大的负担。
  • 当然,也有优化的办法,string相加的时候,string内部持有一个String数组,在展示的时候,才拼成最终的String。但这个改动就比较大了。

要不要设计单独的辅助类来解决字符串拼接问题
那这个辅助类怎么设计,要不要考虑线程安全
如果考虑线程安全的话,怎么兼顾性能

现在看来是要一个辅助类的,比如StringBuilder(非线程安全)和StringBuffer(线程安全),但StringBuilder的线程安全也仅仅是每个方法前面加了synchronized而已啊。

然后说性能,我要append一个长度为1000的String,按理来说String不可变,我可以把String存到数组里,build的时候再拼。

但是StringBuilder的实现还是一个一个char拷进StringBuilder,最后拼的时候,再拷了一次。多copy了一次啊。

说到这儿, @RednaxelaFX 也在回答的评论区提到了:

在Oracle JDK / OpenJDK的实现中,无论用StringBuffer还是StringBuilder去作为字符串拼接的底层实现其实都不是最优的——它们俩都不是为append-only场景优化,而是为更通用的可变字符串场景优化的。

然后我看看如何实现:

List暂存一把最后需要的时候直接造个新string

我先去查了下,Apache Commons和Guava里面居然都没有这个实现,这个让我很吃惊。

但是仔细一想,这个优化第三方库确实很难做:

  • List先转成char[],copy了一次。通过这个char[]来new String的时候,还得再copy一次(String和其他对象共享char[]的构造方法是包级别的,不是public的)
  • 如果要做只能使用反射或者unsafe来强行调用了,这样就和实现绑定了,不通用了。

所以现在看来,String的append操作,确实很难优化。要不将String实现搞复杂;要不上层自己写StringBuilder来做。


参考资料:

作者

Robert Lu

发布于

2019-05-02

许可协议

评论