Java常用类源码——String、StringBuffer、StringBuild源码解析

String

String的定义

String 在Java中是非常常见的一个数据类型了,Java本身对String是一个怎么样的描述呢?
我们来看其中对String的一些解释:

1
2
3
Strings are constant; their values cannot be changed after they
are created. String buffers support mutable strings.
Because String objects are immutable they can be shared.

上面的意思就是说: String字符串是一个常量,String实例创建之后,它的的值是不能被改变的,但是字符缓冲区是支持可变的字符串,因为缓冲区里面的不可变字符串对象们可以被共享。(发生对象的引用)。

再来看String在jdk1.8中的定义

1
2
3
4
5
6
7
8
9
10
11
12
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
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
//..... 等等代码
}

可以看出String 是final类型,表示该类就不能被其他类继承了,同时实现了序列化,比较、字符序列的接口。

Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本的一致性的,在进行反序列化是,JVM会把传过来的字节流中的seriaVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,就可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。

在Java中,被final类型修饰的类是不允许被其他类继承的,被final修饰的变量被复制后是不允许被修改的。

在研究上面的这段源码,我们知道,String 就是用一个char 数组来保存的,这个数组中是被final修饰的。, 同时String 的默认的hashCode(哈希值)值为0

构造器

我们先看看String的构造器源代码:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
* Initializes a newly created {@code String} object so that it represents
* an empty character sequence. Note that use of this constructor is
* unnecessary since Strings are immutable.
*/
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);
}
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
public String(int[] codePoints, int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= codePoints.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > codePoints.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
final int end = offset + count;
// Pass 1: Compute precise size of char[]
int n = count;
for (int i = offset; i < end; i++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
continue;
else if (Character.isValidCodePoint(c))
n++;
else throw new IllegalArgumentException(Integer.toString(c));
}
// Pass 2: Allocate and fill in char[]
final char[] v = new char[n];
for (int i = offset, j = 0; i < end; i++, j++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
v[j] = (char)c;
else
Character.toSurrogates(c, v, j++);
}
this.value = v;
}
// 字节数组构造器
public String(byte bytes[], Charset charset) {
this(bytes, 0, bytes.length, charset);
}
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charsetName, bytes, offset, length);
}

无参数造器

从上面的无参数构造器中可以发现,内部是创建了一个空的字符串对String 的value字符数组进行初始化的,所以这样的方法是会增加不必要的字符串对象(空字符串),所以尽量不要使用如下的方法创建String对象的实例:

1
2
String str = new String();
str = "hello";

字符串参数构造器

从上面的源代码可以看到,传入的字符串直接赋值给了目标字符串的字符数组,和哈希值。所以保证了原来的String字符对象的变不会影响到新的String对象,通过这个方法构造的额字符对象与原来的是一个不一样的对象。

字符数组构造器

源码中给出了字符数组的多种形式的构造器

第一种

第一种的字符数组,将整个字符数组转化为Sting对象,使用的方法是Arrays.copyof(char[], int)将字符数组中的所有字符复制到目标String的value字符数组中。

1
2
3
4
5
6
7
8
9
/**
Arrays.copyof() 源码
*/
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

从这里可以看出,是直接创建一个字符数组,调用系统的数组复制的方法,将新创建的字符数组对象返回, 而系统的数组复制的这个方法是一个native方法。 native方法不是用java实现的了,一般是用C或C++实现的了。

1
2
3
4
// System 类中的arraycop native方法。
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);

字符数组、个数的构造器

从参数中可以看出,第二个参数是偏移量,第三个参数是要送字符数组中转化成Sting对象的字符个数。
在构造方法里面,进行了offset 和count数值的异常检查, 最后调用Arrays.copyOfRange(char[],int, int) 方法对字符数组进行复制。我们来看这个方法是怎么实现的吧:

1
2
3
4
5
6
7
8
9
10
//
public static char[] copyOfRange(char[] original, int from, int to) {
int newLength = to - from;
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
char[] copy = new char[newLength];
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}

从源码中可以知道也是检查了一些常规的数值检查吗,然后创建新的字符数组对象,调用系统的arraycopy()方法进行过复制。

使用字节数组的构造器

在Java中,Sting 实例中保存了一个char[] 字符数组,char[]字符数组是以unicode码来存储的,Sring 和char为内存形式。byte是网络传输或存储的序列化形式,所以在很多的传输和存储的过程中需要将byte[] 数组和String进行相互的转化,所以String提供了一系列重载和构造方法将一个字节数组转化成String,当提到字节数组和String之间的相互转换的就不得不关注编码的问题。出现乱码的问题主要是编码和解码的方式不一样而导致的,所以有指定字符集(charset)来解码指定的byte数组,将其解码成unicode的char[]数组。这样就能避免乱码的问题。
我们从上面的构造方法中我们可以看到,首先进行的是边界的检查,然后调用的是StringCoding.decode(charsetName, bytes, offset, length);的方法,返回字符数组。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//StringDoding.class 的decode()方法
static char[] decode(String charsetName, byte[] ba, int off, int len)
throws UnsupportedEncodingException
{
StringDecoder sd = deref(decoder);
String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
|| csn.equals(sd.charsetName()))) {
sd = null;
try {
Charset cs = lookupCharset(csn);
if (cs != null)
sd = new StringDecoder(cs, csn);
} catch (IllegalCharsetNameException x) {}
if (sd == null)
throw new UnsupportedEncodingException(csn);
set(decoder, sd);
}
return sd.decode(ba, off, len);
}

我们需要知道的是。使用byte[] 字节数组构造String的时候,如果没有指明解码使用的字符集的话,那么StringCoding的decode方法首先会调用系统默认的编码格式,默认是ISO-8859-1编码格式进行操作。

主要方法

hashCode的实现

我们探讨String的hashCode之前先来看看hashCode的官方定义:

hashcode方法返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表.
hashCode 的常规协定是:
在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。
以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。
实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)
当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

由此我们可以知道,hashCode的哈希值最基本的作用是区分不同的对象。那么String的hashCode是怎么实现的呢?我们探究一下这个hashCode的具体实现吧,如下:

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
// String类中的hashCode的实现。
/**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using {@code int} arithmetic, where {@code s[i]} is the
* <i>i</i>th character of the string, {@code n} is the length of
* the string, and {@code ^} indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

从源码中可以知道,String是重写了Object中的hashCode的方法,该方法是native方法,String 中的 hashCode的计算过程是:先判断hash是否被计算过,并且字符串不为空,则进行hashCode计算, 然后计算的hashCode的方式是s[0]*31^(n-1)+s[1]*31^(n-2)+ ... + s[n-1] 通过多项式计算而来,所以我们完全可以通过不同的字符串得出的相同的哈希值,所以两个String对象的hashCode相同,并不代表两个String 是一样的。(可以这么想:既然hashCode都是自己可以重写的,那么我自己设计hashCode值计算方式的时候,可能就会不同对象计算出相同的值。)

trim()

1
2
3
4
5
6
7
8
9
10
11
12
13
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}

intern

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
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java&trade; Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();

intern方法是Native调用,它的作用是在方法区中的常量池中通过equals方法寻找等值的对象,如果没有找到则在常量池中开辟一片空间存放字符串并返回该对应String的引用,否则直接返回常量池中已经存在的String对象的引用。

例如

1
2
3
String st = new String("hello");
String st2 = "hello";
System.out.println(st == st2);

上面的输出是false,原因是st 和st2指向的是不同的两个对象,内存的地址值是不同的。
但是使用intern()方法会有什么样的效果呢?

1
2
3
String st = "hello";
String st2 = new String("hello").intern();
System.out.println(st==st2);

上述代码输出的是true, 因为st指向的地址来自于常量池中,而st2创建字符对象的时候,是从常量池中去寻找是有相同的字符串,如果有,就返回地址,没有就在常量池中创建新的字符串。所以上面的代码输出的是true。

equals

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
35
36
37
38
39
40
41
/**
* Compares this string to the specified object. The result is {@code
* true} if and only if the argument is not {@code null} and is a {@code
* String} object that represents the same sequence of characters as this
* object.
*
* @param anObject
* The object to compare this {@code String} against
*
* @return {@code true} if the given object represents a {@code String}
* equivalent to this string, {@code false} otherwise
*
* @see #compareTo(String)
* @see #equalsIgnoreCase(String)
*/
public boolean equals(Object anObject) {
// 如果引用的是同一个 对象,返回真
if (this == anObject) {
return true;
}
//如果不是String 类型的数据,就返回假
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
//如果char数组长度不相等,返回假
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
//从后先前单个字符判断,如果有不相等的,则返回假
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
// 如果每个字符都是相等,就返回真
return true;
}
}
return false;
}

equals方式是经常用到的,最常用来比较两个字符串字符内容是否是相等的,那么由上面可以知道,String对象的判断规则是:

  1. 内存地址相同,则为真。
  2. 如果对象类型不是String类型,则为假,否则继续判断。
  3. 如果对象长度不相等,则为假否则继续判断。
  4. 从后往前,判断String类中char数组value的单个字符是否相等,有不相等则为假,如果一直到第一个数,则返回真。

compareTo()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
*/
public int compareTo(String anotherString) {
//比较字符串的长度
int len1 = value.length;
//被比较的字符串的长度
int len2 = anotherString.value.length;
//取两个字符串中较短的
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
//从第一个字符串开始比较,如果字符不相等,返回两个字符的差值
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
//较短的比较完之后,还有则返回自身长度- 被比较的对象的长度。
return len1 - len2;
}

String中的compareTo方法的比较方法是基于每个字符的Unicode的值进行比较的,比较的内容是文本的内容。

substring()

substring是一个使用起来非常方便的方法了,在我们对String的日常操作中是经常使用到的。当然这个方法方便的同时,也要小心使用,尤其是方法中的参数,如果参数没有弄清楚,哈哈,那么可能就得不到你想要的结果哦!那么看看String中是怎么实现的吧!图下面的代码所示:

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
35
36
37
//String类中的substring 源码
/**
* Returns a string that is a substring of this string. The
* substring begins at the specified {@code beginIndex} and
* extends to the character at index {@code endIndex - 1}.
* Thus the length of the substring is {@code endIndex-beginIndex}.
* <p>
* Examples:
* <blockquote><pre>
* "hamburger".substring(4, 8) returns "urge"
* "smiles".substring(1, 5) returns "mile"
* </pre></blockquote>
*
* @param beginIndex the beginning index, inclusive.
* @param endIndex the ending index, exclusive.
* @return the specified substring.
* @exception IndexOutOfBoundsException if the
* {@code beginIndex} is negative, or
* {@code endIndex} is larger than the length of
* this {@code String} object, or
* {@code beginIndex} is larger than
* {@code endIndex}.
*/
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}

从上面的代码中可以知道,这个方法做了参数数值的异常处理,然后重要的是在返回上,如果截取的是和原来一致的,那么就返回原来的字符对象,否则的化,构造了一个新的字符串,所以substring都是并没有对原字符串的修改。

注意点:

该方法

trim()

trim()方法也是我们常用的方法,经常用作表单中对用户输入数据的处理,去除前后的空格,怎么做到的呢?直接上源码吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// String类中的trim()方法源码
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
//找到前面没有空格的位置
while ((st < len) && (val[st] <= ' ')) {
st++;
}
//找到后面没有空格的位置
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
// 如果前后没有空格,返回原字符串,否则,通过截取,构建新的字符串,
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}

总结

String 对象是不可变类型,返回类型为String 的String方法除了和原字符串相同的情况下返回原字符串,其他的都是返回新的String对象。
String对象的三种比较方式:

  • == 是内存的比较:直接对比两个引用的内存值,精确简洁直接明了。
  • equals比较: 比较两个引用所指向对象字面值是否相等。
  • hashCode数值比较:将字符串数值化,两个引用的hashCode相同,不保证内存一定相同,不保证字面值一定相同。

StringBuffer

StringBuffer概述

上面讲解了String的源码解析,知道了最基本的一个概念是String对象是不可变的,但是StringBuffer是怎么样的呢?或许你学习过Java的人会有一点认识就是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
/**
* A thread-safe, mutable sequence of characters.
* A string buffer is like a {@link String}, but can be modified. At any
* point in time it contains some particular sequence of characters, but
* the length and content of the sequence can be changed through certain
* method calls.
* <p>
* String buffers are safe for use by multiple threads. The methods
* are synchronized where necessary so that all the operations on any
* particular instance behave as if they occur in some serial order
* that is consistent with the order of the method calls made by each of
* the individual threads involved.
* <p>
* The principal operations on a {@code StringBuffer} are the
* {@code append} and {@code insert} methods, which are
* overloaded so as to accept data of any type. Each effectively
* converts a given datum to a string and then appends or inserts the
* characters of that string to the string buffer. The
* {@code append} method always adds these characters at the end
* of the buffer; the {@code insert} method adds the characters at
* a specified point.
* <p>
*/
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence

从上面对StringBuffer类是final的,那么也就是这个类不能被继承了,从定义中可以看到,StringBuffer是实现了序列化和字符序列。同时继承了抽象的AbstractStringBuilder。
从文档解释上可以得到:StringBuffer是一个线程安全,可变的字符序列。它像String, 但是和String不同的是可以被改变,在任何时候,它包含一些特定的字符序列,但是字符序列的长度和内容是可以通过方法的调用进行改变的。所以说是可变的。
StringBuffer 是线程安全的,想必有写人肯定也听说过,到底是不是呢?看他的解释吧!
从第二段解释中可以看到,StringBuffer是线程安全的,可以载多线程环境下使用。StringBuffer里面的方法进行了同步,所以在任何的实例对象对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
34
35
36
37
38
//StringBuffer类源码中的主要的方法。
// 构造方法里面是默认的16个字符大小加上传入的字符序列长度大小
public StringBuffer(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
//StringBuffer的方法大部分是使用了synchronized方法进行了同步
@Override
public synchronized int length() {
return count;
}
@Override
public synchronized int capacity() {
return value.length;
}
@Override
public synchronized void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > value.length) {
expandCapacity(minimumCapacity);
}
}
/**
* @since 1.5
*/
@Override
public synchronized void trimToSize() {
super.trimToSize();
}
/**
* @throws IndexOutOfBoundsException {@inheritDoc}
* @see #length()
*/
@Override
public synchronized void setLength(int newLength) {
toStringCache = null;
super.setLength(newLength);
}
....

从StringBufferd的源码可以知道,StringBuffer的同步采用的措施主要是使用关键字synchronized对方法进行同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* A cache of the last value returned by toString. Cleared
* whenever the StringBuffer is modified.
*/
// 字符缓冲区
private transient char[] toStringCache;
/** use serialVersionUID from JDK 1.0.2 for interoperability */
static final long serialVersionUID = 3388685877147921107L;
/**
* Constructs a string buffer with no characters in it and an
* initial capacity of 16 characters.
*/
public StringBuffer() {
super(16);
}

从上面源码中的成员变量和构造方法可以知道,StringBuffer的实现还是和String一样是字符数组(通过super(),在父类中有一个字符数组),但是使用默认对的构造方法创建StringBuffer实例的时候是默认字符数组的大小是16个字符的。见如下的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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];
}

父类中有一个value字符数组用于保存里面的字符数据,同时又一个count用来保存数组中已经使用了的个数。

StringBuffer成员变量中有一个字符缓冲区toStringBuffer,这个字符缓冲区主要用于保存最后一次调用toString()时的字符值,当StringBuffer被修改的时候,这个缓冲区将会被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @since JDK1.0.2
*/
//当StringBuffer修改了之后,字符缓冲区是会被清空的
@Override
public synchronized StringBuffer reverse() {
toStringCache = null;
super.reverse();
return this;
}
//当调用toString方法的时候,将会把字符数组中的数据放入缓冲区中。
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}

从上面的源码中可以知道,StringBuffer中大部分的方法是同步的,在会对StringBuffer对象进行修改的方法中,会将StringBuffer中的缓冲区toStringCache清空(赋值为null),但是单调toString()方法的时候,将会把自己的字符数组中数据复制到缓冲区中,然后返一个新的String对象,我们从String源码中也可以看到带字节数组中的构造方法里是这个样的:

1
2
3
public String(char[] vlaue){
this.value = vlaue;
}

从中我们可以看到,使用这个String构造方法构造出来的String对象不没有实际复制字符串,只是把value指向了构造参数,所以这样是一种节省空间和时间的操作。

主要方法

append

append学过java的人肯定是知道这个方法的。这个方法真个是很好用呀!那么它是怎么实现的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}

StringBuffer中的append()方法,调用的都是父类AbstractStringBuilder中的append方法,那父类中的又是怎么实现的呢?看下面的源码:

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
35
36
37
public AbstractStringBuilder append(String str) {
//当str为null时,将会在当前字符串对象的后面添加"null"字符串的。
if (str == null)
return appendNull();
//获取需要添加的字符串的长度
int len = str.length();
// 详见后面的说明
ensureCapacityInternal(count + len);
//将str中的字符串复制到value数组中
str.getChars(0, len, value, count);
//更新当前字符串对象的字符串长度
count += len;
return this;
}
// Documentation in subclasses because of synchro difference
public AbstractStringBuilder append(StringBuffer sb) {
if (sb == null)
return appendNull();
int len = sb.length();
ensureCapacityInternal(count + len);
sb.getChars(0, len, value, count);
count += len;
return this;
}
private AbstractStringBuilder appendNull() {
int c = count;
ensureCapacityInternal(c + 4);
final char[] value = this.value;
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
count = c;
return this;
}

从父类中的append方法中我们可以知道主要是使用父类中的ensureCapacityInternal方法进行字符串的追加的,最后返回对象本地身(this),所以append()可以连续调用的,那么继续追踪ensureCapacityInternal源码是怎么实现的:源码如下

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
35
36
public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > 0)
ensureCapacityInternal(minimumCapacity);
}
/**
* This method has the same contract as ensureCapacity, but is
* never synchronized.
*/
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
//如果需要扩展的容量比当前字符数组长度要大。就扩容
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
/**
* This implements the expansion semantics of ensureCapacity with no
* size check or synchronization.
*/
void expandCapacity(int minimumCapacity) {
// 将初始化的新的容量大小为当前字符串长度的2倍 + 2
int newCapacity = value.length * 2 + 2;
//如果还小的话,那就用最小的容量为新数组的大小
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
//如果新容量大小或者最小容量小于0
//抛出异常并且 设置新容量设置为Integer最能存储的最大值
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
//创建容量大小为newCapacity的新数组
value = Arrays.copyOf(value, newCapacity);
}

从上面的源码解释中可以看出里面是发生了扩容的,最后将value的值进行拷贝。

delete

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

StringBuffter中主要使用的是父类中方法,看看父类中的delete方法是如何实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 父类AbstractStringBuilder 源码中的delete方法
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;
}

在父类中的delete中可以看到,方法里面做了数值的常规检查,然后确定需要删除的长度,然后调用系统的数组复制的方法进行复制,最后返回本对象。

subSting

subString靠父类AbstractStringBuilder实在是太多了。subString也是不例外的,肯定是主要用父类的进行实现了。

1
2
3
4
5
6
7
8
9
10
//StringBuffer的父类源码中的substring中的实现
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);
}

从父类中的方法可以看返回的是一个新的String对象了。与原来的对象是不共享内存的(因为String(char[] ,int, int)是通过this.value = Arrays.copyOfRange(value, offset, offset+count); 使用系统的方法进行拷贝的)。 由于StringBuffer继承于AbstractStringBuilder,所以这个本身不太具有特殊的方法,,这里就不再一一介绍了。

StringBuilder

之前介绍了String、StringBuffer,那么最后一个StringBuilder怎么能够少了呢?

StringBuilder概述

StringBuffer是AbstractStringBuilder的子类,StringBuilder也是它的子类。他们的继承和实现的接口都是一样的。AbstractStringBuilder封装了StringBuffer和StringBuilder的大部分操作的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}

其中要注意一点是StringBuffer中没有字符缓冲区了,那么在toString()的时候就不是从缓冲区中拿取字符串,的而是重新构造一个String对象出来,内存是不共享的,这个和AbstractStringBuilder中的substring()方法是一样的,都是新创建一个String.

如下是StringBuilder的toString方法源码:

1
2
3
4
5
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}

还需要注意一点是,StringBuild与StringBuffer不同的是,StringBuild不是线程安全的,其中的方法没有实现同步,但StringBuffer是线程安全的。

关于String、StringBuffer、StringBuild

三者的区别

String 类型和StringBuffer的主要性能区别:String是不可变的对象, 因此在每次对String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,性能就会降低。

使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。所以多数情况下推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。

在某些特别情况下, String 对象的字符串拼接其实是被 Java Compiler 编译成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,例如:

1
2
String s1 = “This is only a” + “ simple” + “ test”;
StringBuffer Sb = new StringBuilder(“This is only a”).append(“ simple”).append(“ test”);

生成 String s1对象的速度并不比 StringBuffer慢。其实在Java Compiler里,自动做了如下转换:
Java Compiler直接把上述第一条语句编译为:

1
String s1 = “This is only a simple test”;

所以速度很快。但要注意的是,如果拼接的字符串来自另外的String对象的话,Java Compiler就不会自动转换了,速度也就没那么快了,例如:

1
2
3
4
String s2 = “This is only a”;
String s3 = “ simple”;
String s4 = “ test”;
String s1 = s2 + s3 + s4;

这时候,Java Compiler会规规矩矩的按照原来的方式去做,String的concatenation(即+)操作利用了StringBuilder(或StringBuffer)的append方法实现,此时,对于上述情况,若s2,s3,s4采用String定义,拼接时需要额外创建一个StringBuffer(或StringBuilder),之后将StringBuffer转换为String;若采用StringBuffer(或StringBuilder),则不需额外创建StringBuffer。

使用策略

1. 基本的原则:如果要操作少量的数据,用String;单线程操作大量数据,用StringBuilder多线程操作大量数据用StringBuffer
2. 尽量不要使用String类的”+”来进行字符串的拼接,因为这样的操作性能是极差的,应该使用StringBuffer和StringBuild

1
2
3
4
5
6
7
8
9
10
11
String result = "";
for (String s : hugeArray) {
result = result + s;
}
// 使用StringBuilder
StringBuilder sb = new StringBuilder();
for (String s : hugeArray) {
sb.append(s);
}
String result = sb.toString();

3.为了获得更好的性能,在构造StringBuffer或者StringBuilder时指定它们的容量。如果你操作的字符串的长度不超过16个字符就不用了(默认分配给字符数组大小为16个字符)。

4. StringBuilder一般使用在方法内部来完成类似”+”功能,因为是线程不安全的,所以用完了以后可以丢弃,StringBuffer主要用在全局变量中
5. 相同情况下使用StrigBuilder相比使用StringBuffer仅获得10%~15%左右的性能提升,但是却要冒线程不安全的风险,而现实的模块化编程中,负责某一模块的程序员不一定清晰的判断该模块是否会放入多线程的环境中运行,因此:除非确定系统的瓶颈是在StringBuffer上,并且确定你的模块不会运行在多线程模式下,才可以采用StringBuilder;否则还是用StringBUffer

链接:http://blog.csdn.net/kingzone_2008/article/details/9220691
链接:http://docs.Oracle.com/javase/tutorial/java/data/buffers.html
链接:http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.4

坚持原创技术分享,您的支持将鼓励我继续创作!