String源码浅谈

String这个类可以说是我们使用得最为频繁的类之一了,前几次去面试,都被问到String的底层源码,回答得都不是很好,今天就来谈谈一下String的源码。

一、String类

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

String类被是被final修饰的,表明该类不可被继承,有关于关键词final的作用,请移步我的另外一篇文章​​【JAVA】关键词final的作用​​。值得一提的是,StringBuilder,StringBuffer类都是被final修饰的。


二、String类的属性

/** 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;

(1)char value[]

String底层的存储结构是一个字符类型的数组,同样也是被final修饰,因此一旦这个字符数组被创建后,value变量不可再指向其他数组,但是可以改变value数组中某一个元素的值。

(2)int hash

hash用来保存某一个String实例自己的哈希值,可以说是哈希值的一个缓存,因此String特别适合放入HashMap中,作为key来使用。每次插入一个键值対时,不需要重新计算key的哈希值,直接取出key的缓存hash值即可,在一定程度上,加快了HashMap的效率。

(3)long serialVersionUID

用于保证版本一致性。由于String实现了Serializable接口,因此需要拥有一个序列化的ID。序列化时,将此ID与对象一并写入到文件中,反序列化时,检测该类中的ID与文件中的ID是否一致,一致的话,说明版本一致,序列化成功。


三、String类的构造函数

(1)无参构造函数,创建一个空字符串,即"",用得地方不多。

public String() {
this.value = new char[0];
}

(2)接收一个String实例的构造函数

public String(String original) {
this.value = original.value;
this.hash = original.hash;
}

(3)接收一个字符数组,利用Arrays.copyOf()方法进行拷贝

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

进入到Arrays.copyOf()方法中,发现调用的是System.arraycopy()方法,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;
}

而System.arraycopy()方法是一个本地方法,由其他语言实现。

从以上源码,可以看得出,这个构造方法没有直接使用传入的字符数组的引用,而是使用该数组的一个拷贝,保证了String类的不可变性。我们无法通过在外部改变此数组中某些元素的值,来改变构造后的String的值。

同样在toCharArray()方法中,也是返回一个基于字符数组的拷贝,并没有直接直接返回value数组。

public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}

另外,有关于System.arraycopy()、Arrays.copyOf()之间的效率问题,可以参考我的另外一篇文章​​【JAVA】数组复制效率的比较​


(4)接收一个字符数组,从offset位置开始复制,一共选取count位

public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// 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);
}

其他的构造函数的原理大同小异,这里就不再说明了。


四、String类的其他方法

(1)equals()方法

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

String类重写了equals()方法,判断两个String实例代表的字符串是否相同。

判断规则:

如果两个Stirng实例压根就是一个对象,即它们的内存地址相同,则直接返回true。

之后,对anObject进行类型判断,类型为String后,继续判断,否则直接返回false。

再对两者的长度进行判断,如果相等,继续判断,否则返回false。

两者长度相等后,再从前往后依次比较两者字符数组中的元素是否相等,全相等的后,返回true。

equals()方法一上来没有直接比较两个字符串的字符数组元素,在比较超长的字符串时,节省了大量的时间。


(2)compareTo()方法

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类中还有一个内部类CaseInsensitiveComparator,其中也有一个compare()方法,与compareTo()方不同的是,compare()进行比较时,会忽略两个字符串的大小写。


(3)hashCode()方法

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类同样也重写了hashCode()方法,用于计算String实例的哈希值。

哈希值相同的两个字符串不一定相同,相同的字符串的哈希值一定相同。

至此,String类重写了equals(),也重写了hashCode()方法。关于两个方法之间的关系,可以参考我的另外一篇文章​​【JAVA】为什么重写equals(),就必须要重写hashCode()?​


(4)intern()方法

public native String intern();

可以看得出,它是一个本地方法。

当调用此方法时,会首先在方法区中的常量池中使用equals()寻找是否存在此字符串,如果存在,直接返回此字符串的引用。如果不存在时,会首先将此字符串添加到常量池中,再返回该字符串的引用。

下面通过一个例子来说明

package day1203;

//String类的源码
public class StringTest {
public static void main(String[] args) {
String a = "abc";//这个abc在常量池中创建
String b = new String("abc");//这个abc在堆上创建
String c = b.intern();//在常量池中寻找是否存在abc,若存在的话,直接返回它的引用。
System.out.println(a == b);//返回false
System.out.println(a == c);//返回true
}
}

这里涉及字符串的创建与存储方式,可以参考我的另外一篇文章​​【JAVA】字符串的创建与存储机制​


五、总结

String类中还有很多有趣的操作,比如字符串的截取、匹配、替换、大小写转换、分割等操作,这里都没有涉及。这些操作确实都是经常用到的,相信大家也能够理解他们的用法,这里就不再赘述了。

如果还有一些疑问,也欢迎大家在下方评论,我一定会及时回复。