扩展 - String、StringBuilder、StringBuffer
# String、StringBuilder、StringBuffer
可变性:
- String 不可变
- StringBuilder 和 StringBuffer 可变
线程安全性:
- String 线程安全
- StringBuilder 线程不安全
- StringBuffer 线程安全
String 类是构造和操作字符串的一个类,是典型的 immutable 类,其类、方法、属性都被修饰为 final,原生的支持线程安全。
场景: 由于 String 的不可变性,在对字符串进行操作时,当改变原字符串内容都会产生一个新的字符串对象。而当对字符串使用重载 +
或 +=
频繁操作,会产生大量需要垃圾回收的中间对象
String strByConcat = "aa" + "bb";
解决方案: StringBuffer 与 StringBuilder 都是为解决 String 在拼接过程中产生过多的中间对象而设计的
String strByBuilder = new
StringBuilder().append("aa").append("bb").toString();
2
它们都继承自 AbstractStringBuilder 类,使用 char 数组存放字符串数据(JDK9 之后使用 byte 数组)。不同之处在与 StringBuffer 是线程安全的,实现方式是在 StringBuilder 修改数据的相关方法前添加 synchronized 关键字,所以 StringBuffer 的性能不如 StringBuilder,如果不存在线程安全问题则推荐使用 StringBuilder 类。
StringBuffer 和 StringBuilder 在构造初始化 char 数组的默认长度是16,如果初始化有输入字符串则长度为 输入字符串长度 + 16,也可以在构造时指定初始化长度,在已知需要拼接的大致长度,通过直接初始化相应长度的 StringBuffer 或 StringBuilder 可以避免多次扩容(每次扩容都需要通过 Arrays.copy() 函数复制并创建新的数组);
扩容规则位:当前长度 x 2 + 2
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
2
3
4
5
在实际中,以下两种写法的效率是一样的,因为 Java 虚拟机的编译时对代码进行了优化,将通过 + 符号连接的字符串操作编译为 StringBuilder 操作:
String strByConcat = "aa" + "bb";
StringBuilder sb = new StringBuilder();
String strByBuilder = sb.append("aa").append("bb").toString();
2
3
4
通过 javap
反编译结果如下:
需要注意:在循环体内使用 String 重载 +
或 +=
,在每次循环编译器都会创建一个新的 StringBuilder 对象性,为避免这种情况,建议在循环体外自己创建一个 StringBuilder 对象,用来构建最终的结果。
场景:实际应用中一般都存在大量的字符串对象,如果能够减少重复创建相同的字符串,就可以有效降低内存消耗和对象创建开销。
解决方案:
缓存方式:
而 Java 6 提供 intern() 方法,通过调用该方法将字符串缓存起来(缓存位置为 PermGen 永久代),所以如果大量使用 intern 方法会存在 OOM 的风险故不被推荐使用,而后续版本中缓存位置放在了堆中,避免了永久代被占满的问题,而 Java 8 则把缓存位置放在元数据区 MetaSpace 中,缓存大小也在不断变大。
在实际运行时为了优化,字符串的一些基础操作会直接利用 JVM 内部的 Intrinsic 机制,往往运行的就是特殊优化的本地代码,而根本就不是 Java 代码生成的字节码。
存储方式:
在 Java 9 前字符串 String 是通过 char 数组存放的,在 Java 中 char 占用2个byte,实际语言中根本不需要太宽的 char,从而造成一定的浪费。而在 Java 9 对 String 进行了重构,引入 Compact Strings 设计,将字符串数据存储方式由 char 数组改为了 byte 数组。虽然这样导致在同样数组长度下,存储容量降低了一倍,但这种紧凑型字符串带来的是:更小的内存占用,更快的操作速度。