什么是不可变对象?
在面向对象及函数编程语言中,不可变对象(英语:Immutable object)是一种对象,在被创造之后,它的状态就不可以被改变。至于状态可以被改变的对象,则被称为可变对象(英语:mutable object)。–来自百度百科
Java8 String源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
...
}
显然String字符串内部是使用char[]数组来存储的
而这个char[]数组是用 private final来修饰的,private就体现着面向对象的封装特性,并且String没有提供供外部访问的方法,这就意味着这个属性无法被外部访问;final则意味着这个属性无法修改,无法重新指向其他对象。且String 类没有提供/暴露修改这个字符串的方法。
因此,String是不可变对象
设计成不可变的优点
- 线程安全。同一个字符串实例可以被多个线程共享,因为字符串不可变,本身就是线程安全的。
- 支持hash映射。因为String的hash值经常会使用到,比如作为 Map 的键,不可变的特性也就使得hash值不会变,不需要重新计算。
- 字符串常量池优化。String对象创建之后,会缓存到字符串常量池中,下次需要创建同样的对象时,可以直接返回缓存的引用。
String, StringBuffer 和 StringBuilder区别
- 可变性
- String不可变
- StringBuffer 和 StringBuilder 可变
- 线程安全
- String 不可变,因此是线程安全的
- StringBuilder不是线程安全的
- StringBuffer 是线程安全的,内部使用 synchronized 进行同步
StringBuffer的append方法
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
为什么拼接字符串建议使用StringBuilder,而不用String直接拼接
源码
String源码,存放字符串的地方
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}
StringBuilder自身没有自定义存储的容器,而是继承了其父类的容器
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**The value is used for character storage.*/
char[] value;
...
}
运算时区别
String
String s1 = "第1个字符串";
String s2 = "第2个字符串";
String str = s1 + s2;
以上操作可以看成是
//这里只作为理解,相当于新开拓一个字符数组,然后复制
final char c1[] = {'第','1','个','字','符','串'};
final char c2[] = {'第','2','个','字','符','串'};
final char c3[] = new char[12];
c3[] = {'第','1','个','字','符','串','第','2','个','字','符','串'};
- 创建s1的时候其实就是创建了第一个不可变的char[]数组,创建s2的时候创建了第二个不可变的char[]数组
- 创建str的时候其实就是另外又创建了一个数组,再将s1和s2的数据复制到str中
StringBuilder
StringBuilder sb = new StringBuilder();
System.out.println("初始容量:" + sb.capacity());
sb.append("十五个十五个十五个十五个十五个");
System.out.println("追加15个字后sb容量:" + sb.capacity());
sb.append("一");
System.out.println("已经十六个字SB容量:" + sb.capacity());
sb.append("添加");
System.out.println("超过16个字的SB容量:" + sb.capacity());
输出
初始容量:16
追加15个字后sb容量:16
已经十六个字SB容量:16
超过16个字的SB容量:34
StringBuilder特征:
StringBuilder初始化容量是16(无参构造)
public StringBuilder() {
super(16);
}
- 追加之前会计算一次容量,大于所需容量则会重新创建一个char[]数组,计算规则是 newCapacity = (value.length << 1) + 2; 也就是原来长度*2 + 2
- StringBuilder在运算的时候每次会计算容量是否足够,如果所需容量不小于自身容量,那么就会重新分配一个自身容量两倍 +2 的char[].
//追加操作
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//count是当前char[]数组的使用大小,len是要追加的字符串的长度
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//这个方法是确保 char[]数组的大小能装下新追加的字符串
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
//判断所需要的容量是否小于char[]数组的容量
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));//如果小于,就扩容,并拷贝数组内容
}
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;//扩容数组大小,也就是原来长度*2+2
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
因此,如果再一次追加的时候容量足够,就无需创建新数组,也就省去了很多创建char[]的次数.
小结:
- String之所以慢是因为,大部分cpu资源都被浪费在分配资源拷贝资源的部分了,相比StringBuilder有更多的内存消耗。
- StringBuilder快就快在,相比String,他在运算的时候分配内存次数小,所以拷贝次数和内存占用也随之减少,当有大量字符串拼接时,StringBuilder创建char[]的次数会少很多。
- 由于GC的机制,即使原来的char[]没有引用了,那么也得等到GC触发的时候才能回收,String运算过多的时候就会产生大量垃圾,消耗内存。
建议
- 如果目标字符串需要大量拼接的操作,那么这个时候应当使用StringBuilder.
- 反之,如果目标字符串操作次数极少,或者是常量,那么就直接使用String.