String 源码分析

String

String 字符串是常量,其值在实例创建后就不能被修改

string.png

通过注释跟继承关系,我们知道String被final修饰,而且一旦创建就不能更改,并且实现了CharSequence,Comparable以及Serializable接口。

final

  • 修饰类:当用final修饰一个类时,表明这个类不能被继承。也就是说,String类是不能被继承的,
  • 修饰方法:把方法锁定,以防任何继承类修改它的含义。
  • 修饰变量:修饰基本数据类型变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

String类通过final修饰,不可被继承,同时String底层的字符数组也是被final修饰的,char属于基本数据类型,一旦被赋值之后也是不能被修改的,所以String是不可变的。

CharSequence

CharSequence翻译就是字符串,但是他是一个接口,CharSequence主要提供一些对字符序列的只读访问,许多类如StringBuilder、StringBuffer也都实现了此接口,里面就只有几个方法:

1
2
3
4
5
6
public interface CharSequence {
int length();
char charAt(int index);
CharSequence subSequence(int start, int end);
public String toString();
}

String 的成员变量

1
2
3
4
5
private final char value[]; // 底层char数组

private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];//存储对象的序列化信息

private int hash; // Default to 0 缓存String的 hash Code,默认值为 0

String 的构造方法

1
2
3
public String() {
this.value = "".value;
}

此方法很简单,是一个空字符的value拷贝到对象中,在实际中基本没有用到这个构造方法

1
2
3
4
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}

这个方法将 original 的value和hash拷贝至新建对象中。

1
2
3
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}

将value字符数组拷贝至新建对象中

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
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);
}
// count等于零,偏移量不大于value数组的长度(即在char的范围内),则返回空字符
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// 偏移量和count越界抛出异常
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
// 调用 copyOfRange 方法,复制字符数组到新建对象的数组中
this.value = Arrays.copyOfRange(value, offset, offset+count);
}

这个构造方法是传入一个char数组,偏移量以及个数来创建对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
//字符编码参数为空,抛出空指针异常
if (charsetName == null)
throw new NullPointerException("charsetName");
// 判断是否越界
checkBounds(bytes, offset, length);
//调用 decode方法将字节数组从offset开始截取length个长度解码为字符串,
this.value = StringCoding.decode(charsetName, bytes, offset, length);
}

private static void checkBounds(byte[] bytes, int offset, int length) {
if (length < 0)
throw new StringIndexOutOfBoundsException(length);
if (offset < 0)
throw new StringIndexOutOfBoundsException(offset);
if (offset > bytes.length - length)
throw new StringIndexOutOfBoundsException(offset + length);
}
这个方法和上个方法类似,并且可以指定字符编码
1
2
3
4
5
6
7
public String(StringBuffer buffer) {
// 对buffer加锁
synchronized(buffer) {
//拷贝 StringBuffer 字符数组给当前实例的字符数组
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}

此方法给buffer加锁,对通过buffer新建String进行同步。

1
2
3
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}

此方法与上一个类似,但是没有加锁,因此不是线程安全的,但是速度会更快一些

String 中的重要方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 注意 String 的length是方法,有括号的,而数组的length是属性,并没有括号
public int length() {
return value.length;
}

public char charAt(int index) {
// 判断是否越界
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
// 返回value数组相应下标的字符
return value[index];
}

public boolean isEmpty() {
return value.length == 0;
}

compareTo 方法

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

compareTo 方法是比较从左往右第一个不同地方的字符的差值,如果除了长度前面都一样,则返回两个长度的差值,如果长度相同,返回0。

equals方法与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
27
28
29
30
31
32
33
34
35
36
37
38
39
public boolean equals(Object anObject) {
// 判断是否是同一个对象,如果是直接返回true
if (this == anObject) {
return true;
}
// 如果不是同一个对象,判断是否都是 String 的对象,如果不是直接返回false
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
// 如果都是 String 的对象,判断长度是否相同,如果不同,直接返回false
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 如果长度相同,逐个字符判断是否相同,如果有不同直接返回false
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
// 如果每个字符都相同,返回true
return true;
}
}
return false;
}

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

在上述的代码分析中,并没有提及hash方法以及对hash变量的操作,所以在调用hashCode方法前hash变量都是0,在hashCode方法中对hash进行计算,方法对value的每个字符进行h = 31 * h + val[i];计算。之所以选择31作为乘子呢,是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是一个传统。同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 * i == (i << 5) - i,现代的 Java 虚拟机可以自动的完成这个优化。在执行完hashCode方法后,会将结果缓存至hash变量中。

String 中的 “+”

1
String str = "I'm " + "Creams";

在执行上述代码的时候,编译器会创建 StringBuilder 对象,为每个加法操作字符串调用一次append方法,最后调用toString方法再赋值给str字符串。但是需要注意的是,如果由于每执行一行加法运算就会创建一次 StringBuilder 对象,所以如果需要大量的字符串操作的话,还是使用 StringBuffer 或者 StringBuilder 对象进行字符串操作,最后再用toString方法得到String对象。

参考资料