String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 immutable 类,被声明为 final class,所有的属性也是 final 的。由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所有相关操作的效率往往对应用性能有明显影响。

StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。 StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。

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

如果继续深入,面试可从各种不同的角度考察,如:

  • 通过 String 和相关类,考察基本的线程安全设计与实现,各种基础编程实践;
  • 考察 JVM对象缓存机制的理解以及如何良好地使用;
  • 考察 JVM 优化 Java 代码的一些技巧;
  • String 相关类的演进,比如 Java 9 中实现的巨大变化;

知识扩展

字符串设计和实现考量

String 是 Immutable 类的典型实现,原生的保证了基础线程安全,无法对它内部数据进行任何修改。Immutable 对象在拷贝时不需要额外复制数据。而 StringBuffer的线程安全是通过把各种修改数据的方法都加上 synchronized 关键字实现的。

为实现修改字符序列的目的,StringBuffer 和 StringBuilder 底层都利用可修改的(char,JDK 9 以后是byte)数组,二者都继承了 AbstractStringBuilder ,里面包含了基本操作,区别仅在于最终的方法是否加了 synchronized 。
内部数组构建时初始字符串长度为16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是16)。如果确定拼接会发生非常多次,且大概可预计的,就可指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行 arraycopy

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

字符串缓存

如果能避免创建重复字符串,可以有效降低内存和对象创建开销。
String 在 Java6 以后提供了 intern() 方法,目的是 提示 JVM 把相应字符串缓存起来,以备重复使用。如果已经有缓存的字符串,就会返回缓存里的实例,否则将其缓存起来。一般来说,JVM 会将所有的类似 “abc” 这样的文本字符串,或者字符串常量之类缓存起来。
但结果并不和我们预料的一样。被缓存的字符串存于所谓的 PermGen,即“用久代”,这个空间是很有限的,也基本不会被 FullGC 之外的垃圾收集照顾到。所以如果使用不当,就会经常发生OOM

在后续版本中,这个缓存被放在了堆中,这样就极大地避免了永久代占满的问题。甚至永久代在 JDK8 中被 MetaSpace(元数据区) 替代了。而且,默认缓存大小也在不断地扩大中,从最初的 1009,到 7u40 以后被修改为 60013.可用以下参数查看:

  1. -XX:+PrintStringTablesStatistics

也可用以下参数手动调整大小:

  1. -XX:StringTableSize=N

Intern 是一种显式地排重机制, 但也有一定副作用:(1) 需要开发者显式地调用,很麻烦;(2) 很难保证效率,应用开发阶段很难清楚地预计字符串的重复情况。

在 Oracle JDK8u20 之后,推出一个新特性:G1 GC 下的字符串排重。即通过将相同数据的字符串指向同一份数据来做到的,是 JVM底层的改变,不需要 Java 类库做什么修改。
可使用以下参数开启,记得指定使用 G1 GC:

  1. -XX:+UseStringDeduplication

在运行时,字符串的一些基础操作会直接利用 JVM 内部的 Intrinsic 机制,往往运行的是特殊化的本地代码,而不是 Java 代码生成的字节码。Intrinsic 可以理解为 一种利用 native 方式 hard-coded的逻辑,是一种特别的内联。很多优化还是需要直接使用特定的 CPU 指令。

String自身的演化

在历史版本中,字符串使用 char 数组来存数据,因为 char 是两个 bytes 大小,而如果是拉丁语系语言的字符,不需要太宽的 char,这样无区别的实现就会造成一定的浪费。
在 Java 6时 JDK 就提供了压缩字符串的特性,但因为在实践中出现一些问题,在最新JDK版本中已移除掉。
Java9中引入了 Compact String的设计,将数据存储方式从 char 数组改为了一个 byte 数组加上一个标识编码的 coder,并将相关字符串操作类进行了修改。所有相关的 Intrinsic 之类也都进行了重写,以保证没有任何性能损失。

虽然底层实现出现了巨大变化,但Java字符串的行为并没任何大的变化,这个特性对绝大部分应用都是透明的。
在极端情况下,字符串也会出现一些能力退化,比如最大字符串的大小。原来 char 数组的实现,字符串的最大长度就是数组本身的长度限制,但替换成 byte数组,同样数组长度下,存储能力退化了一倍。还好现实应用还没发现受影响。