首先一句话
String是不可变的,StringBuilder和StringBuffer是可变的,StringBuffer是线程安全的,而StringBuilder是非线程安全的。
String的不可变性
存储结构
String对象在内部使用一个 char 数组来存储字符串数据。从Java 9开始,String 类使用 byte 数组加上一个编码标记(coder)来存储字符串,以更有效地处理不同的字符集。但不管是哪种存储方式,这个数组被声明为 final,这意味着一旦数组被初始化,它的引用就不能指向另一个数组。
构造过程
当你创建一个新的 String 对象时,例如通过字符串字面量(如 “hello”)或通过构造函数,Java 会在内存中创建一个新的 String 对象,并初始化这个内部数组。因为内部数组是 final 的,所以一旦字符串被赋值,就无法更改这个数组的内容。
操作行为
当你对字符串进行任何修改的操作,如拼接、替换字符等,Java 实际上并不是修改原有的字符串,而是创建了一个新的 String 对象。例如,使用 + 操作符连接两个字符串时,Java 实际上会在内存中创建一个新的 String 对象来保存结果,原来的字符串对象不会被改变。
String s1 = "hello";
String s2 = s1 + " world";
在这个例子中,s1 指向 “hello” 字符串的内存位置。当执行连接操作 s1 + " world" 时,实际上会在内存中创建一个新的字符串 “hello world”,而 s1 仍然指向原来的 “hello”。这就保证了 s1 的不变性。
为什么要设计成不可变的?
主要有以下方面考虑:
- 安全性:因为字符串不可变,所以它们可以安全地用在多线程环境中,无需担心并发修改。
- 缓存:不变性允许 String 对象被缓存,例如字符串字面量都是缓存并重用的。
- 使用为哈希键:不变性保证了字符串的哈希码(hashCode)是常量,这使得字符串非常适合作为哈希表的键。
- 性能:因为字符串不可变,所以可以用字符串池缓存,可以大大节省堆内存。而且还可以提前对哈希码(hashCode)进行缓存,更加高效,由于字符串是应用最广泛的数据结构,提高字符串的性能对提高整个应用程序的总体性能有相当大的影响。
StringBuilder的可变性、非线程安全性
StringBuilder简化源码如下:
public final class StringBuilder implements java.io.Serializable, CharSequence {
// 字符数组,用于存储字符串内容
private char[] value;
// 当前StringBuilder中字符的数量
private int count;
// 构造方法,初始化StringBuilder对象
public StringBuilder() {
value = new char[16]; // 初始容量为16
}
// 向StringBuilder中追加字符
public StringBuilder append(String str) {
// 检查字符数组容量是否足够,如果不够则进行扩容
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
// 扩容方法,保证字符数组容量足够
private void ensureCapacityInternal(int minimumCapacity) {
// 如果当前字符数组的长度不够,则进行扩容
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
// 扩容方法,将字符数组容量扩大一倍
private void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
}
可变性:StringBuilder的可变性体现在其内部的字符数组(char[] value)上,且没有用final进行修饰。在初始化StringBuilder时,会创建一个指定初始容量的字符数组,并且随着字符的追加、插入或删除操作,该字符数组的长度可能会动态变化,但是StringBuilder对象本身并不会改变。这种设计使得可以在同一个StringBuilder对象上执行多个操作,而不会创建新的对象,从而提高了性能。
线程不安全性:StringBuilder的线程不安全性主要源自于其内部状态的修改没有进行同步控制。在StringBuilder的实现中,并没有对字符数组的修改进行同步操作(即没有synchronized修饰),因此当多个线程同时访问和修改同一个StringBuilder实例时,可能会导致竞态条件(race condition)的发生,进而产生不可预测的结果。这种情况下,如果一个线程正在执行修改操作,而另一个线程同时进行了修改或读取操作,可能会导致数据不一致或异常。
StringBuffer的可变性、线程安全性
同样上简化源码进行分析:
public final class StringBuffer implements java.io.Serializable, CharSequence {
// 字符数组,用于存储字符串内容
private char[] value;
// 当前StringBuffer中字符的数量
private int count;
// 构造方法,初始化StringBuffer对象
public StringBuffer() {
synchronized(this) {
value = new char[16]; // 初始容量为16
}
}
// 向StringBuffer中追加字符,方法使用synchronized关键字进行同步
public synchronized StringBuffer append(String str) {
// 检查字符数组容量是否足够,如果不够则进行扩容
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
// 扩容方法,保证字符数组容量足够
private void ensureCapacityInternal(int minimumCapacity) {
// 如果当前字符数组的长度不够,则进行扩容
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
// 扩容方法,将字符数组容量扩大一倍,方法使用synchronized关键字进行同步
private synchronized void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
}
可变性:可以发现,StringBuffer与StringBuilder非常相似,因此可变性的原理是一样的。
线程安全性:StringBuffer的方法是同步的,因此它是线程安全的。这种线程安全性是通过在StringBuffer的方法中使用synchronized关键字来实现的,这样就可以确保在多线程环境中对StringBuffer的操作是同步的,不会发生竞态条件。
性能:虽然这种同步机制确保了线程安全性,但也降低了性能,因为在多线程环境下可能会出现线程竞争,导致一些线程需要等待其他线程完成操作。
选择策略
选择合适的类取决于项目的需求,如果不确定是否需要线程安全性,或者仅在单线程环境下操作字符串,可以优先选择StringBuilder,因为它具有更好的性能。如果需要保证线程安全性,或者在多线程环境下操作字符串,可以选择StringBuffer。而对于不需要修改的字符串内容,则可以直接使用String。