这里我们只针对Java中的String、StringBuilder和StringBuffer的主要特征做相关描述,并通过源码对其实现原理做相应的解释。
String
我们都知道,在Java中String变量的内容是不可变的,字符串的内容在创建的时候就已经写死,后续对String的修改操作都是在拷贝原理字符串的基础上进行修改的,原来的字符串会不再被引用,直到被GC为止。那么为什么字符串的内容能保持不变呢?我们可以看一下源码:
/** 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类主要定义了两个基本属性,其中value[]
被定义成一个final类型的私有字符数组,该字符数组用来存储字符串的实际内容,并且String类只提供了初始化方法来直接操作该数组,因此该字符数组一旦被初始化,其内容将无法修改(因为压根就没有修改这个数组的接口)。下面我们通过几个主要的构造方法来详细解释。
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
String的无参构造方法String()
,直接初始化了一个内容为空的字符串(有别于null),并将当前的value指向该空字符串的value。而String(String original)
更是直接将当前要初始化的字符串的value和hash指向了原始字符串的value和hash内容(注意:这里没有直接return original,因为如果接下来将original指向null,当前字符串就会直接变成null)。String(char value[])
方法直接将String内部的value指向了当前传递的字符数组的副本,这样做得目的,同样是为了防止当前数字的改变会引起字符串的内容改变。因此,我们可以看出来,Java中的String对象的内容是不可变的。
StringBuilder
StringBuilder提供了一种可变的不保证线程安全的字符序列的实现方式。该类继承自AbstractStringBuilder
,很多关于操作都是直接调用父类的方法,我们不妨先看一下它的父类AbstractStringBuilder
的实现原理。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
/**
* This no-arg constructor is necessary for serialization of subclasses.
*/
AbstractStringBuilder() {
}
/**
* Creates an AbstractStringBuilder of the specified capacity.
*/
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
...
}
AbstractStringBuilder
内部定义了一个默认可见性的字符数组,用于存储内容,另外还有一个默认的整型count,表示当前数组中字符的个数(注意:不是当前字符数组的大小,count<= value.length)。AbstractStringBuilder
只提供了两个构造方法,无参的构造方法默认什么都不做,而构造方法AbstractStringBuilder(int capacity)
只做了一件事,初始化当前字符数组的大小。而真正对当前AbstractStringBuilder
的内容进行增加的是append()
方法。我们只对其中一个方法public AbstractStringBuilder append(String str)
进行详细解释:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
如果传入的参数为null,直接追加null,如果传入的参数不为空,首先要保证当前数组的容量能够容纳下所有字符的存储。这里用到了一个方法ensureCapacityInternal(count + len)
。其代码如下:
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
其实也很好理解,如果容积充足,什么都不做,否则拷贝当前字符数组,并进行扩容。在保证字符数组容积冲突的前提下,将传入的字符串的所有字符依次追加到当前字符数组的后面,实现如下:
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);
}
因此我们知道了AbstractStringBuilder
能做到动态追加字符的原理在于不断扩容和追加新字符。至此我们解释完了AbstractStringBuilder
的主要功能的实现原理,下面我们看看StringBuilder
的相关实现:
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
...
public StringBuilder() {
super(16);
}
public StringBuilder(int capacity) {
super(capacity);
}
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
...
}
通过上述代码,我们可以看到:StringBuilder
的初始化和动态追加字符的功能都是依赖于它的父类AbstractStringBuilder
,因此,StringBuilder
也能像AbstractStringBuilder
一样动态追加字符。但是需要注意的一点是:StringBuilder
的append()
方法是线程不安全的,在高并发的情况下,有可能会出现意想不到的结果。因此在高并发的情况下,我们更需要线程安全的操作字符序列的类,当然,JDK为我们提供了这样一个线程安全的类,这个类就是StringBuffer
。
StringBuffer
StringBuffer
与StringBuilder
一样都是继承自AbstractStringBuilder
,其常用方法的实现如下:
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
...
/**
* A cache of the last value returned by toString. Cleared
* whenever the StringBuffer is modified.
*/
private transient char[] toStringCache;
public StringBuffer() {
super(16);
}
public StringBuffer(int capacity) {
super(capacity);
}
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
...
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
...
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
}
StringBuffer
定义了一个私有的transient类型的变量toStringCache
,该变量只是在最终返回字符串的时候用于缓存,其他时候一旦当前对象进行改变,该数组都会被置为null,方便垃圾回收。
StringBuffer
实现同步的原理很简单,就是将AbstractStringBuilder
的一些append
方法以synchronized
同步的方式进行了重写。因此, StringBuffer
在追加内容的时候可以保证线程安全,但是由于加了同步机制,每次追加的时候都必须先获取锁,然后再追加字符,然后再释放锁,因此其性能会大打折扣。所以在追求高性能和不要求线程安全的情况下,我们推荐使用StringBuilder来创建动态的字符串,强烈不推荐字符串String
以+
的方式进行追加。
总结
- String的内容是不可变的。
- StringBuilder提供了一种线程不安全的动态字符串修改的实现方式,但是效率较高。
- StringBuffer提供了一种线程安全的动态字符串修改的实现方式,但是效率相对较低,每次追加都需要不断地获取锁和释放锁。
网友评论