StingBuffer 和 StringBuilder 的区别

参考文献:

  1. 极客时间《Java核心技术面试精讲》
  2. OpenJDK / JDK

为什么会有 StringBuffer 类?

String 是一个典型的 Immutable 类,被声明为 final class,所有的属性也都是 final 的。具有不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所有相关操作的效率往往对应用性能有明显的影响。

StringBuffer 是为解决这个问题而提供的一个类,使用 append 或者 add 方法,把字符串窜添加到已有序列的末尾或者指定位置。


StringBuffer 和 StringBuilder 的区别

StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。

StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

StringBuffer 和 StringBuilder 底层都是利用可修改的(char,JDK 9 以后是 byte)数组,二者都继承了 AbstractStringBuilder,里面包含了基本操作,区别仅在与最终的方法是否加了 synchronized。


在声明字符串时,对字符串长度理解和如何设计?

在创建内部数组时,如果太小,拼接的时候可能要重新创建足够大的数组;如果太大,又会浪费空间。目前的实现是,构建时初始字符串长度加 16(这意味着如果没有构建对象时输入最初的字符串,那么初始值就是 16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的数组,还要进行 arraycopy。


在没有线程安全的情况下,全部拼接操作是都应该用 StringBuilder 实现吗?

非静态的拼接逻辑在 JDK8 中会自动被 javac 转换为 StringBuilder 操作;而在 JDK9 里面,则是体现了思路的变化。 Java9 利用 InvokeDynamic,将字符串拼接的优化与 javac 生成的字节码结构,假设未来 JVM 增强相关运行时实现,将不需要依赖 javac 的任何修改。

在日常变成过程中,保证程序的可读性、可维护性,往往比所谓的最优性能跟重要,你可以根据实际需求酌情选择具体的编码方式。


字符串缓存的优化

把常见应用进行堆转储(Dump Heap),然后分析对象组成,会发现平均 25% 的对象是字符串,并且其中约半数是重复的。如果能避免创建重复字符串,可以有效降低内存消耗和对象开销。

String 在 Java6 以后提供了 intern() 方法,目的是提示 JVM 把响应字符串缓存起来,以备重复使用。在我们创建字符串并调用 intern() 方法的时候,如果已经有缓存的字符串,就会返回缓存里的实例,否则将其缓存起来。一般来说,JVM 会将所有类似“abc”这样的文本字符串,或者字符串常量之类缓存起来。

然而上面的处理方法并不理想,被缓存的字符串是存在所谓的 PermGen 里的,这个空间是很有限的,也基本不会被 FullGC 之外的垃圾收集器照顾到,所以如果使用不当,会经常出现 OOM。

在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题,甚至永久代在 JDK8 中被 MetaSpace(元数据区)替代了。而且默认缓存大小也在不断地扩大中,从最初的 1009,到 7u40 以后被修改为 60013。以使用下面的参数直接打印具体数字,可以拿自己的 JDK 立刻试验一下。

-XX:+PrintStringTableStatistics

你也可以使用下面的 JVM 参数手动调整大小,但是绝大部分情况下并不需要调整,除非你确定它的大小已经影响了操作效率。

-XX:StringTableSize=N

Intern 是一种显示地排重机制,但是它也有一定的副作用,因为需要开发者写代码时明确调用,一是不方便,每一个都显示调用是非常麻烦的;另外就是我们很难保证效率,应用开发阶段很难清楚地预计字符串的重复情况,有人认为这是一种污染代码的实践。

幸好在 Oracle JDK 8u20 之后,推出了一个新特性,也就是 G1 GC 下的字符串排重。它是通过将相同数据的字符串指向同一份数据来做到的,是 JVM 底层的改变,并不需要 Java 类库做什么修改。这个功能是默认关闭的,需要使用下面的参数开启,并且记得指定使用 G1 GC:

-XX:+UseStringDeduplication

前面说到的几个方面,只是 Java 底层对字符串各种优化的一角,在运行时,字符串的一些基础操作会直接利用 JVM 内部的 Intrinsic 机制,往往运行的就是特殊优化的本地代码,而根本就不是 Java 代码生成的字节码。Intrinsic 可以简单理解为,是一种利用 native 方式 hard-coded 的逻辑,算是一种特别的内联,很多优化是需要直接使用特定的 CPU 指令,具体可以看相关的 源码


String 自身的演化

在历史版本中,字符串使用 char 数组来存数据,这样非常直接。但是 Java 中的 char 是两个 bytes 大小,拉丁语系语言的字符,根本就不需要太宽的 char,这样无区别的实现就造成了一定的浪费。密度是编程语言平台永恒的话题,因为归根接地绝大部分任务是要来操作数据的。

其实在 Java9 中,我们引入了 Compact Stings 的设计,对字符串进行了大刀阔斧的改进。将数据存储方式从 char 数组,改变为一个 byte 数组加上一个标识编码的所谓 coder,并且将相关字符串操作类都进行了修改。另外,所有相关的 Instinct 之类也都进行了重写,以保证没有任何性能损失。

虽然底层发生了这么大的改变,但是 Java 字符串的行为并没有任何打的变化,所以这个特性对绝大部分应用来说是透明的,绝大部分情况不需要修改已有代码。

当然,在极端情况下,字符串也出现一些能力退化,比如最大字符串的大小。原来 char 数组的实现,字符串的最大长度就是数组本身的长度限制,但是替换成 byte 数组,同样数组长度下,存储能力是退化了一倍的。还好这是存在于理论中的极限,还没有发现现实应用受此影响。

在通用的性能测试和产品实验中,我们能非常明显地看到紧凑字符串带来的优势,即更小的内存占用、更快的操作速度。

上一篇:Springboot 中@Controller 和 @RestController 的区别


下一篇:c语言表白程序代码