问题:
- String是如何保证不可变的?
- String为什么要设计为不可变的?
- StringBuffer为什么可变?底层逻辑是什么?
- StringBuffer是如何进行扩容的?
String是如何保证不可变的?
String类被final修饰,不能被继承。底层采用一个 private final char value[] 数组进行存储,并且使用final修饰,所以String是不可变的。
- final修饰的不能被继承;
- final修饰的变量赋值后不能改变;
- final修饰的方法不能重写;
但是这里要明确一点,字符数组被 final 修饰,不能改变的是变量的引用地址,并不是字符数组的中的元素。可以对字符数组中的元素进行改变。
public static void main(String[] args) throws Exception {
final char[] chars = new char[]{'A', 'B', 'C'};
chars[0] = 'D';
System.out.println(chars);
}
既然字符数组是可变的,那为什么String又是不可变的呢?其实更多的是在底层实现的。通过源码可以发现 char 数组是 private 修饰的,并且没有对外部提供任何修改 char 数组的方法。String被 final 修饰不可被继承,也无法通过继承重写方法的方式进行破坏。通过这三种手段保证了 String 的不可变性。
String 为什么设计为不可变的?
更容易实现字符串池
字符串池是用来存储Java常量的一块空间。主要用来缓存和重用字符串对象,从而提高性能和节省内存。当我们声明一个字符串常量时,会先检查字符串池是否存在相同内容的字符串常量。如果存在,则直接返回字符串池中的对象引用;如果不存在,则在字符串池中创建一个新的字符串对象,并返回对象的引用。
保证多线程安全
并发场景下,多个线程读取同一个资源并不会引发线程问题的。但是多个线程对同一资源进行写操作是不安全的,所以 String 通过它的不可变性,从而保证了多线程的安全问题。
避免安全问题
在网络连接和数据库连接中经常使用字符串作为参数,例如,网络连接地址URL,反射机制所需要的 String 参数,其不变性可以保证连接的安全性。如果字符串是可变的,那么可以通过修改字符串指向对象的值进行破坏,可能会引起严重的安全问题。
加快字符处理速度
因为 String 不可变,保证了 hashcode 的唯一性,那么在创建对象时就可以将 hashcode 缓存,不需要重新参与计算。这也是 Map 喜欢使用 String 作为 Key 的原因,处理速度比其他的键对象要快。所以 HashMap 经常选用 String 作为键。
StringBuilder:
非线程安全,StringBuilder底层采用一个可变的 char[] 数组。也就是说StringBuilder底层这个 char 数组是可以进行扩容的。
改变字符串的底层逻辑:
StringBuilder 使用了 append() 进行了字符串的拼接,底层是使用了数组拷贝的方式进行数据的赋值,将要拼接的字符数组 copy 到原字符数组中去,并且会在 copy 之前校验原数组是否需要扩容。
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
// 检验是否需要扩容
ensureCapacityInternal(count + len);
// 数组copy
str.getChars(0, len, value, count);
count += len;
return this;
}
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
StringBuilder扩容:
StringBuilder在初始化的时候,默认是char数组长度是16,每次扩容是原char数组长度的是 2n+2。
StringBuffer:
线程安全,相比 StringBuilder 来说在原来的方法上使用了 synchronized 来保证线程安全。
性能比较:
StringBuilder > StringBuffer > String
总结:
如果遇到大量的字符串拼接的时候,优先选择使用StringBuilder或StringBuffer。 String是字符串常量,不可变,使用String进行字符串操作的时候,每次都会创建一个新的对象,原来的对象就会变成垃圾被GC回收掉,很影响效率。 StringBuilder和StringBuffer它们是字符串变量,是可变的。当对字符串进行操作时,实际上实在对象上操作,并不会像String一样每次创建新的对象。