jdk1.8源码填坑计划-3-String

String类就是我们日常用的最多的那个String。这不是一个基础数据类型,但是却是很重要的一个。

String的结构

1
2
3
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
......
}

和Integer一样这个也是final的,同样是一个不可变对象(immutable objects),同时这个还实现了Comparable接口用来比较大小,还实现了CharSequence用来做有序的字符序列。

1
2
3
4
5
/** 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就是这么存储的。

构造方法

Structure of String

String的构造方法可真多。

1
2
3
4
5
6
7
8
9
10
11
12
 /**
* Allocates a new {@code String} so that it represents the sequence of
* characters currently contained in the character array argument. The
* contents of the character array are copied; subsequent modification of
* the character array does not affect the newly created string.
*
* @param value
* The initial value of the string
*/
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}

1
2
3
4
5
6
7
8
9
10
/*
* Package private constructor which shares value array for speed.
* this constructor is always expected to be called with share==true.
* a separate constructor is needed because we already have a public
* String(char[]) constructor that makes a copy of the given char[].
*/
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}

第二个方法是包内使用的,这种数组的直接赋值,虽然是线程不安全的,但是速度要比第一个快很多,这个方法被Integer.toString()使用。

equals方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
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;
}

说白了就是挨个比较,没有什么特殊的。

hashCode 方法

String的hashCode还是很有意思的,特别是那个31。

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

注释中写了hashcode的计算公式:s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]
这肯定是个推敲了很久的算法。31也是一个神奇的数字 一方面乘以它可以转换为31 * i = (i << 5) - i,另外一方面这个不大不小的素数,是非常棒的保证Hash均匀分布的因子。
具体可以参见这个文章String hashCode 方法为什么选择数字31作为乘子

concat方法

1
2
3
4
5
6
7
8
9
10
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}

这个方法把传进来的参数拼接在原对象的前面,然后new一个对象出来,原对象不变。

indexOf replace split 等方法

这几个统统都是挨个循环来判断的,没什么好说的。其他的方法也没有什么特殊的,所以我就不再写了
不过对于replaceAll我倒是还有点兴趣,真不知道正则表达式是如何做的。
占坑 java是如何实现正则表达式的。

String的常量池

这里才是这篇文章的重头戏。

常量池

1
2
3
4
5
String str1 = "hello";
String str2 = "hello";
String str3 = new String("hello");
System.out.println(str1 == str2);
System.out.println(str1 == str3);

答案是 true和false
第一行的这种“hello”会被存入常量池。JVM开启的时候会开辟一个新的空间:String Pool,这个就是所说的字符串常量池。这个思路就和Integer的缓存思想类似,String池用来存放运行时中产生的各种字符串,并且池中的字符串的内容不重复。
这样当String str2 = “hello”;执行的时候会先从常量池里面找,找到了就把常量池引用直接赋给str,找不到的话就在常量池中创建一个。这么做的好处是避免重复创建对象。
当String str3 = new String(“hello”);执行的时候会直接在堆上创建一个新对象,引用的是新对象的地址。
由于引进了常量池,这些常量池中的数据不可被修改,又不会被GC,大大避免了频繁的创建字符串对象。

intern方法

这是一个本地方法

1
2
3
4
5
6
7
8
/* 
* When the intern method is invoked, if the pool already contains a
* string equal to this object as determined by
* the equals(Object) method, then the string from the pool is
* returned. Otherwise, this object is added to the
* pool and a reference to this object is returned.
*/
public native String intern();

当一个String对象调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于这个对象值的字符串并返回它的引用。
注意这里是在常量池中增加一个等于对象值的字符串,而不是把这个对象直接加进去。

为什么要设计这个不可变对象String

日常开发中String是不可或缺的,但是为什么要把String设计成一个不可变对象呢?
字符串常量池只有String是不可变的这一前提才有意义。
另外加入String是可变的,那么String的数值每次修改,其hashCode都会进行重新计算,那么对于Map Set这一类的集合,他们的键值唯一性就会收到破坏,所以必须设计一个不可变的对象来作为这一类集合的键值,String明显最适合这一点。
在并发场景下,多个线程同时读写资源,会引发竞态条件,由于String是不可变的,不会引发线程问题进而保证了线程安全。

一个疑惑

hash是在哪一个时刻计算的?比如

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

这里hash重新计算了吗?

PS. 这是第三篇,我拖了12个小时,拖延症千万不要来啊。

葛祥海
2019-03-05

疑问得到的解惑

正经的不可变对象包含了以下条件

对象创建后其状态不能修改。
对象的所有域都是final。
对象是正确创建的(this没有溢出)。

按理说第二条是可以划掉的,因为String就不满足第二条,那是咋做得到呢?String每次修改都是一个new String,并且String计算散列值被推迟到第一次调用hashCode时才进行,计算结果存在了非final的类型中,之所以可行是因为他的hash算法和存储的值有关,每次计算hash得到的值都是确定的,而且有默认值。
所以说答案是String不进行计算Hash,而是等到第一次调用hashCode()时才把hash计算出来。

葛祥海
2019-04-02

0%