Java源码阅读笔记03 - 字符串三剑客

  在Java语言规范中,处理一个字符串可以通过三个类 — String、StringBuilder、StringBuffer来实现,三者之间在增删改字符串内容时采用的方法和原理有很多不一致的地方,这里将简单论述上述三个类型的异同。本文演示代码段的执行环境基于JDK版本1.7

概述

  在String、StringBuilder、StringBuffer中,String被定义为不可变类型,也就是说String对象value字段一经赋值就不会被改变,直至生命周期结束被垃圾回收机制回收。而后两者是可变的,并且都提供了对应的API以便开发人员可以根据需要修改其所包含的内容。但是后两者也有一些区别,StringBuilder不适用于多线程环境,StringBuffer可以在多线程环境中保证数据一致。鉴于此,StringBuilder的处理效率相对于StringBuffer来说会更乐观一些。

继承关系

1
2
3
4
5
6
7
8
//StringBuilder/StringBuffer
--java.lang.Object
--java.lang.AbstractStringBuilder
--java.lang.StringBuilder/StringBuffer

//String
--java.lang.Object
--java.lang.String

实现接口

类名 实现接口
String Serializable, CharSequence, Comparable<String>
StringBuilder/StringBuffer Serializable, Appendable, CharSequence

部分方法

Constructor Summary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public StringBuilder() {
super(16);
}

public StringBuilder(int capacity) {
super(capacity);
}

public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}

public StringBuilder(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}

  StringBuilder中有四个构造方法,代码中第一个构造方法是个无参构造方法,这个方法内部会调用super()方法然后执行 AbstractStringBuilder的构造方法AbstractStringBuilder(int capacity),这个方法里会初始化一个类型为char数组的value字段,并指定value的长度为16位。行5 - 7的构造方法通过传入一个容量值来指定value字段的长度。行9 - 17的两个有参构造方法在入参长度的基础上延长十六位来初始化value字段的长度,然后通过调用append()方法向value字段赋值。所以,StringBuilder会在实例化对象时预留十六个长度来实现内容的增删改操作。

  StringBuffer同样有四个构造方法,方法声明和内部执行过程和StringBuilder相同,这里不予赘述。

public StringBuilder append(String str)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public StringBuilder append(String str) {
super.append(str);
return this;
}

// 行2中调用的是继承的父类AbstractStringBuilder的append方法,代码执行如下:
public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}

private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}

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);
}

  如代码,在AbstractStringBuilder类中实现了一个append()方法用来将入参字符串追加到现有字符串尾部。如果入参字符串为空,那么会把null追加到当前字符串尾部。通过方法ensureCapacityInternal()判断当前字符串是否有足够的空间来容纳入参字符串,如果发现空间不足,那么就调用expandCapacity()方法进行扩容操作。之后将入参字符串加入到当前字符串尾部,更新当前字符串长度值,并返回最终字符串内容。由于StringBuilder继承了AbstractStringBuilder类,于是对append()方法进行了重写同时调用父类AbstractStringBuilder的append()方法计算拼接后的StringBuilder对象value值。

  StringBuffer中append(String str)方法的执行和StringBuilder相同,唯一不同的是StringBuffer在append(String str)的方法声明中加入了关键字synchronized来表示该方法在执行时需要进行锁定,待执行完毕后释放锁供其他线程调用。StringBuffer中的方法声明如下:

1
2
3
4
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}

public StringBuilder append(Object obj)

1
2
3
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}

  该方法将obj转成String对象,然后调用append(String str)获取最后结果。

public StringBuilder append(CharSequence s)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public StringBuilder append(CharSequence s) {
if (s == null)
s = "null";
if (s instanceof String)
return this.append((String)s);
if (s instanceof StringBuffer)
return this.append((StringBuffer)s);
if (s instanceof StringBuilder)
return this.append((StringBuilder)s);
return this.append(s, 0, s.length());
}

/**
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public StringBuilder append(CharSequence s, int start, int end) {
super.append(s, start, end);
return this;
}

public AbstractStringBuilder append(CharSequence s, int start, int end) {
if (s == null)
s = "null";
if ((start < 0) || (start > end) || (end > s.length()))
throw new IndexOutOfBoundsException(
"start " + start + ", end " + end + ", s.length() "
+ s.length());
int len = end - start;
ensureCapacityInternal(count + len);
for (int i = start, j = count; i < end; i++, j++)
value[j] = s.charAt(i);
count += len;
return this;
}

  由于String、StringBuffer、StringBuilder都有继承CharSequence类,所以需要分类判断根据入参类型执行对应的append()方法。如果发现入参不属于String、StringBuffer、StringBuilder三者中的任意一个,那么AbstractStringBuilder也有对应的方法供使用。在append(CharSequence s, int start, int end)方法中,首先做各种边界判断条件并减产当前字符串对象的空余容量是否能包含入参的所有内容。最后循环遍历入参内容指定范围内的内容并加入到value字段中返回。

  同理,StringBuffer也是通过在方法声明中加入关键字synchronized来实现多线程之间的数据访问控制,方法声明如下:

1
2
3
4
public synchronized StringBuffer append(CharSequence s, int start, int end) {
super.append(s, start, end);
return this;
}

public StringBuilder reverse()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public StringBuilder reverse() {
super.reverse();
return this;
}

public AbstractStringBuilder reverse() {
boolean hasSurrogate = false;
int n = count - 1;
for (int j = (n-1) >> 1; j >= 0; --j) {
char temp = value[j];
char temp2 = value[n - j];
if (!hasSurrogate) {
hasSurrogate = (temp >= Character.MIN_SURROGATE && temp <= Character.MAX_SURROGATE)
|| (temp2 >= Character.MIN_SURROGATE && temp2 <= Character.MAX_SURROGATE);
}
value[j] = temp2;
value[n - j] = temp;
}
if (hasSurrogate) {
// Reverse back all valid surrogate pairs
for (int i = 0; i < count - 1; i++) {
char c2 = value[i];
if (Character.isLowSurrogate(c2)) {
char c1 = value[i + 1];
if (Character.isHighSurrogate(c1)) {
value[i++] = c1;
value[i] = c2;
}
}
}
}
return this;
}

  这个方法是StringBuilder和StringBuffer类中独有的方法,用来逆序反转一个字符串序列。例如,对字符串”abcde”调用方法reverse()后的结果似”edcba”。方法执行的核心逻辑在类AbstractStringBuilder的同名方法reverse()中,在代码行9 - 18中,作者循环遍历原始字符串的一半内容(左一半),完成该位置和原始字符串右一半中对应位置的字符值交换。因为原始字符串中可能包含由代理对构成的字符,所以在循环遍历的过程记录是否需要做代理对的转换,即hasSurrogate。在Unicode中表示中,一个代理对由两个字节构成,高位代理(Surrogate High)和低位代理(Surrogate Low),且高位代理在高位字节,低位代理在低位字节,所以在行9 - 18循环遍历完之后就变成了低位代理在左,高位代理在右。因此如果hasSurrogate为true,那么在行19 - 31中再次循环交换后的字符串,调整高位代理和低位代理的左右顺序以保证字符串内容的一致。

  StringBuffer中该方法的执行逻辑和StringBuilder一致,唯一的区别是在方法声明中加入了关键字synchronized以保证多线程环境中数据的访问控制。具体方法声明如下:

1
2
3
4
public synchronized StringBuffer reverse() {
super.reverse();
return this;
}

public StringBuilder delete(int start, int end)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public StringBuilder delete(int start, int end) {
super.delete(start, end);
return this;
}

public AbstractStringBuilder delete(int start, int end) {
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (end > count)
end = count;
if (start > end)
throw new StringIndexOutOfBoundsException();
int len = end - start;
if (len > 0) {
System.arraycopy(value, start+len, value, start, count-end);
count -= len;
}
return this;
}

  删除原始字符串中从start位置开始,end位置(不包含第end位置)结束的子字符串内容,并维护最终的字符串长度值。StringBuffer中操作逻辑同StringBuilder相同,不予赘述。方法声明如下:

1
2
3
4
public synchronized StringBuffer delete(int start, int end) {
super.delete(start, end);
return this;
}

public StringBuilder insert(int offset, String str)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public StringBuilder insert(int offset, String str) {
super.insert(offset, str);
return this;
}

public AbstractStringBuilder insert(int offset, String str) {
if ((offset < 0) || (offset > length()))
throw new StringIndexOutOfBoundsException(offset);
if (str == null)
str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
System.arraycopy(value, offset, value, offset + len, count - offset);
str.getChars(value, offset);
count += len;
return this;
}

  在原始字符串的指定位置中插入入参字符串。首先做容量判断,如果当前空余容量不足以容纳全部的入参字符串的话就需要进行扩容操作。然后将入参字符串加入到原始字符串中,并维护更新原始字符串的长度值。StringBuffer中操作逻辑同StringBuilder相同,不予赘述。方法声明如下:

1
2
3
4
public synchronized StringBuffer insert(int offset, String str) {
super.insert(offset, str);
return this;
}

public synchronized void trimToSize()

1
2
3
4
5
6
7
8
9
public synchronized void trimToSize() {
super.trimToSize();
}

public void trimToSize() {
if (count < value.length) {
value = Arrays.copyOf(value, count);
}
}

  在StringBuffer类的设计中,有的时候存在StringBuffer的初始化的value长度会大于实际存储的字符数,也就是说value字段中会有空余空间。这种情况下为了节约存储空间会考虑将value字段的空余空间移除。

public synchronized String substring(int start)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public synchronized String substring(int start) {
return substring(start, count);
}

public synchronized String substring(int start, int end) {
return super.substring(start, end);
}

//最终调用:AbstractStringBuilder.substring(int start, int end)
public String substring(int start, int end) {
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (end > count)
throw new StringIndexOutOfBoundsException(end);
if (start > end)
throw new StringIndexOutOfBoundsException(end - start);
return new String(value, start, end - start);
}

  StringBuffer中提供了一个substring()方法供开发人员调用,旨在提供一种途径获取当前字符串中的一部分字符串内容并返回。两个方法都加入了关键字synchronized,表明上述方法是多线程安全的。

StringBuffer序列化/反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* Serializable fields for StringBuffer.
*
* @serialField value char[]
* The backing character array of this StringBuffer.
* @serialField count int
* The number of characters in this StringBuffer.
* @serialField shared boolean
* A flag indicating whether the backing array is shared.
* The value is ignored upon deserialization.
*/
private static final java.io.ObjectStreamField[] serialPersistentFields =
{
new java.io.ObjectStreamField("value", char[].class),
new java.io.ObjectStreamField("count", Integer.TYPE),
new java.io.ObjectStreamField("shared", Boolean.TYPE),
};

private synchronized void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
java.io.ObjectOutputStream.PutField fields = s.putFields();
fields.put("value", value);
fields.put("count", count);
fields.put("shared", false);
s.writeFields();
}

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
java.io.ObjectInputStream.GetField fields = s.readFields();
value = (char[])fields.get("value", null);
count = fields.get("count", 0);
}

StringBuilder序列化/反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
s.defaultWriteObject();
s.writeInt(count);
s.writeObject(value);
}

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
count = s.readInt();
value = (char[]) s.readObject();
}

关于String/StringBuilder/Buffer的区别

  首先,String是不可变的,String对象一旦创建,其内容便不会被二次修改。尽管String类提供了多种API来修改字符串内容,但是最终的执行结果是保留了当前字符串内容的同时又创建了一个对象,该对象包含的是修改后的内容值。而StringBuilder和StringBuffer可以对当前字符串自身做增删改操作,最终返回的也是当前字符串自身。

  其次,就执行效率来说,StringBuilder > StringBuffer > String,所以如果在不需要考虑线程安全的情况下,StringBuilder是最快速的字符串处理方案。如果是在多线程的场景下,则可以通过使用StringBuffer来保证线程安全。而由于String具有不可变性,所以在用String对象参与hash表计算时,String的不可变性可以保证hash的key值唯一不变。

  关于三者的使用,还是要分情况具体场景具体分析,不能一味地采用StringBuilder以追求高效率,也可以在一些不需要考虑多线程的场景中用StringBuilder来取代StringBuffer来提高效率。如果字符串处理较少而且比较稳定的话,可以考虑用String来实现。

涉及基础知识点

  1. synchronized关键字的相关知识背景

参考文献

  1. NIL



------------- End of this article, thanks! -------------


  版权声明:本文由Nathan R. Lee创作和发表,采用署名(BY)-非商业性使用(NC)-相同方式共享(SA)国际许可协议进行许可,转载请注明作者及出处。
  本文作者为 Nathan R. Lee
  本文标题为 Java源码阅读笔记03 - 字符串三剑客
  本文链接为 https://marcuseddie.github.io/2018/Java-source-String-StringBuilder-StringBuffer.html