Java String 类的底层实现探秘
Java String 类的底层实现探秘
String 类的定义与不可变性
在 Java 中,String
类被广泛用于处理文本数据。它的定义如下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 类的主体部分
}
从定义可以看出,String
类被声明为 final
,这意味着它不能被继承。这是为了保证 String
对象的不可变性。
不可变性是 String
类的一个核心特性。一旦 String
对象被创建,其内容就不能被改变。例如:
String str = "Hello";
str = str + " World";
在上述代码中,表面上看是对 str
进行了修改,但实际上并非如此。当执行 str = str + " World";
时,会在内存中创建一个新的 String
对象 "Hello World"
,而原来的 "Hello"
对象依然存在于内存中,str
变量只是重新指向了新的对象。
这种不可变性带来了很多好处。首先,它使得 String
对象可以被安全地共享。例如,在字符串常量池中,相同的字符串字面量会被复用,避免了内存的浪费。其次,不可变对象是线程安全的,多个线程可以放心地使用同一个 String
对象而无需担心数据竞争问题。
String 类的底层存储结构
String
类的底层是通过字符数组来存储字符串内容的。在 JDK 9 之前,String
类的定义中有一个 char
类型的数组 value
:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
// 其他成员变量和方法
}
value
数组被声明为 private final
,这保证了 String
对象的不可变性。private
修饰符防止外部直接访问和修改数组,final
修饰符确保数组引用一旦初始化就不能再改变。
从 JDK 9 开始,为了减少内存占用,String
类的底层存储结构发生了变化。value
数组从 char
类型改为了 byte
类型,并增加了一个 coder
字段来标识编码方式:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final byte[] value;
private final byte coder;
// 其他成员变量和方法
}
在大部分情况下,字符串只包含 Latin - 1 字符(即每个字符可以用一个字节表示),此时 coder
的值为 0
,value
数组直接存储 Latin - 1 编码的字符。如果字符串包含非 Latin - 1 字符,coder
的值为 1
,value
数组采用 UTF - 16 编码存储字符。
这种优化使得 String
对象在存储 Latin - 1 字符时,内存占用减少了一半。
字符串常量池
字符串常量池是 Java 中一个重要的概念,它用于存储字符串字面量。当程序中出现字符串字面量时,JVM 会首先检查字符串常量池中是否已经存在相同内容的字符串。如果存在,则直接返回常量池中该字符串的引用;如果不存在,则在常量池中创建一个新的字符串对象,并返回其引用。
例如:
String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2); // 输出 true
在上述代码中,str1
和 str2
都指向字符串常量池中的同一个 "Hello"
对象,所以 str1 == str2
返回 true
。
字符串常量池的存在可以大大节省内存空间,因为相同的字符串字面量只会在常量池中存储一份。
字符串常量池的实现原理
在 HotSpot JVM 中,字符串常量池是一个 StringTable
,它本质上是一个哈希表。当创建一个字符串字面量时,JVM 会计算该字符串的哈希值,并在 StringTable
中查找是否存在相同哈希值且内容相同的字符串。如果存在,则返回其引用;如果不存在,则将新的字符串插入到 StringTable
中。
字符串常量池的大小是有限的,可以通过 -XX:StringTableSize
参数来调整。默认情况下,StringTable
的大小是 60013。
字符串常量池与 intern() 方法
String
类提供了一个 intern()
方法,它的作用是将字符串对象添加到字符串常量池中。如果常量池中已经存在相同内容的字符串,则返回常量池中该字符串的引用;否则,将当前字符串对象添加到常量池中,并返回其引用。
例如:
String str1 = new String("Hello");
String str2 = str1.intern();
String str3 = "Hello";
System.out.println(str1 == str2); // 输出 false
System.out.println(str2 == str3); // 输出 true
在上述代码中,str1
是通过 new
关键字创建的字符串对象,它存储在堆内存中。调用 str1.intern()
方法后,会将 "Hello"
字符串添加到字符串常量池中,str2
指向常量池中的 "Hello"
对象。而 str3
直接指向字符串常量池中的 "Hello"
对象,所以 str2 == str3
返回 true
,str1 == str2
返回 false
。
String 类的常用方法实现
length() 方法
length()
方法用于返回字符串的长度。在 JDK 9 之前,其实现如下:
public int length() {
return value.length;
}
在 JDK 9 之后,由于底层存储结构的变化,实现变为:
public int length() {
return value.length >> coder;
}
这里通过右移操作(>>
)来根据 coder
的值计算实际的字符个数。如果 coder
为 0
(即 Latin - 1 编码),右移操作不改变值;如果 coder
为 1
(即 UTF - 16 编码),右移一位相当于除以 2,因为每个 UTF - 16 字符占用两个字节。
charAt(int index) 方法
charAt(int index)
方法用于返回指定索引位置的字符。在 JDK 9 之前:
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
在 JDK 9 之后:
public char charAt(int index) {
if ((index < 0) || (index >= (value.length >> coder))) {
throw new StringIndexOutOfBoundsException(index);
}
return isLatin1()? StringLatin1.charAt(value, index) : StringUTF16.charAt(value, index);
}
这里先检查索引是否越界,然后根据 coder
的值判断编码方式,调用相应的 StringLatin1.charAt()
或 StringUTF16.charAt()
方法来获取字符。
substring(int beginIndex, int endIndex) 方法
substring(int beginIndex, int endIndex)
方法用于返回从 beginIndex
到 endIndex - 1
的子字符串。在 JDK 9 之前:
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);
}
在 JDK 9 之后:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length >> coder) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
byte[] v = (beginIndex == 0 && endIndex == value.length) ? value : Arrays.copyOfRange(value, beginIndex << coder, endIndex << coder);
return new String(v, coder);
}
这里同样先进行边界检查,然后根据 coder
的值计算子字符串的字节范围,并通过 Arrays.copyOfRange()
方法复制相应的字节数组,最后创建一个新的 String
对象。
equals(Object anObject) 方法
equals(Object anObject)
方法用于比较两个字符串的内容是否相等。其实现如下:
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;
}
首先检查两个对象是否是同一个引用,如果是则直接返回 true
。然后检查 anObject
是否是 String
类型,如果不是则返回 false
。接着比较两个字符串的长度,如果长度不同则返回 false
。最后逐字符比较两个字符串的内容,如果都相同则返回 true
,否则返回 false
。
字符串拼接的实现原理
在 Java 中,字符串拼接是一个常见的操作。有多种方式可以实现字符串拼接,不同方式的底层实现原理也有所不同。
使用 + 运算符
当使用 +
运算符进行字符串拼接时,在编译期,Java 编译器会将其转换为 StringBuilder
对象的 append()
方法调用。例如:
String str1 = "Hello";
String str2 = " World";
String result = str1 + str2;
上述代码在编译后等价于:
String str1 = "Hello";
String str2 = " World";
StringBuilder sb = new StringBuilder();
sb.append(str1);
sb.append(str2);
String result = sb.toString();
StringBuilder
类是一个可变的字符串类,它通过 append()
方法将多个字符串追加到内部的字符数组中,最后通过 toString()
方法将其转换为不可变的 String
对象。
使用 StringBuilder 和 StringBuffer
StringBuilder
和 StringBuffer
类都提供了 append()
方法用于字符串拼接。它们的主要区别在于 StringBuffer
是线程安全的,而 StringBuilder
是非线程安全的。
StringBuilder
的 append()
方法实现如下:
public StringBuilder append(String str) {
super.append(str);
return this;
}
它调用了父类 AbstractStringBuilder
的 append()
方法:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
首先检查 str
是否为 null
,如果是则调用 appendNull()
方法。然后获取 str
的长度,通过 ensureCapacityInternal()
方法确保内部字符数组有足够的容量来存储追加的字符串。接着通过 getChars()
方法将 str
的字符复制到内部字符数组中,并更新 count
(已使用的字符数)。
StringBuffer
的 append()
方法实现类似,但在方法声明上添加了 synchronized
关键字,以保证线程安全:
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
由于线程安全的开销,StringBuffer
的性能通常比 StringBuilder
低,在单线程环境下,应优先使用 StringBuilder
进行字符串拼接。
总结
通过对 Java String
类底层实现的深入探讨,我们了解了其不可变性、底层存储结构、字符串常量池以及常用方法和字符串拼接的实现原理。这些知识对于编写高效、健壮的 Java 程序非常重要。在实际开发中,我们应该根据具体需求合理使用 String
类及其相关的操作,充分利用其特性,避免潜在的性能问题和内存浪费。同时,随着 JDK 的不断发展,String
类的实现可能会继续优化,我们也需要关注这些变化,以更好地适应新的特性和改进。
希望通过本文的介绍,读者对 Java String
类有了更深入的理解,能够在实际编程中更加得心应手地使用字符串相关的功能。