深度剖析JDK 8中String:源码级解读与实战探秘
一、String类概述
在Java编程中,String
类是使用最为频繁的类之一。在JDK 8中,String
类位于java.lang
包下,无需额外导入即可使用。它实现了java.io.Serializable
、Comparable<String>
和CharSequence
接口,这表明String
类支持序列化、可以进行比较以及具备字符序列的基本操作。
(一)String类的特性
- 不可变性:
String
类被声明为final
类,这意味着它不能被继承。同时,其内部用于存储字符内容的char
数组value
也被private
和final
修饰,这使得String
对象一旦创建,其值就不能被更改。例如:
String str = "hello";
str = "world";
在上述代码中,变量str
的值由“hello”变成“world”,但实际上是str
指向了一个新的String
对象,原来的“hello”对象依然存在于内存中。
- 共享性:由于
String
的不可变性,相同的字符串可以被多个String
对象共享。例如:
String s1 = "abc";
String s2 = "abc";
这里的s1
和s2
指向的是字符串常量池中的同一个“abc”对象。
- 序列化支持:
String
类实现了Serializable
接口,这使得String
对象可以在网络传输或持久化存储时进行序列化和反序列化操作。 - 可比较性:实现了
Comparable<String>
接口,因此可以使用compareTo
方法来比较两个String
对象的大小。
(二)String类的内存结构
1. 字符串常量池(String Table)
在JDK 8中,字符串常量池位于堆内存中,被所有线程共享,由垃圾回收器(GC)进行管理。以字面量方式定义的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM都只会建立一个String
对象,并在字符串池中维护。例如:
String s3 = "abc";
String s4 = "abc";
这里的s3
和s4
指向的是字符串常量池中的同一个“abc”对象。而通过new
关键字创建的String
对象,每次都会在堆内存中申请新的内存空间,即使内容相同。例如:
char[] chs = {'a', 'b', 'c'};
String s1 = new String(chs);
String s2 = new String(chs);
这里的s1
和s2
虽然内容相同,但它们在堆内存中是两个不同的对象。
2. 面试题分析:String s = new String(“abc”);
创建对象,在内存中创建了几个对象?
答案是两个。一个是堆空间中new
出来的String
对象,另一个是char[]
对应的常量池中的数据:“abc”。
3. intern()
方法
intern()
方法用于手动将String
对象添加到字符串常量池中。如果常量池中已经存在与该String
对象相等的字符串(通过equals
方法判断),则返回常量池中的字符串;否则,将该String
对象添加到常量池中,并返回对该对象的引用。例如:
String s1 = new String("abc");
String s4 = s1.intern();
这里的s4
会指向字符串常量池中的“abc”对象。
二、JDK 8 String源码分析
(一)类定义和成员变量
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
}
value
数组:用于存储String
对象的字符内容。由于被final
修饰,该数组的引用不可变,且private
修饰限制了外部对该数组的访问,同时没有提供对应的getter
和setter
方法,因此String
对象的内容不可被修改。hash
变量:用于缓存字符串的哈希码,默认值为0。当调用hashCode
方法时,如果hash
值为0,则会重新计算哈希码并赋值给hash
。
(二)常用构造方法
1. 无参构造方法
public String() {
this.value = "".value;
}
无参构造方法会创建一个空的String
对象,其value
数组指向空字符串的value
数组。
2. 有参构造方法:参数为String
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
该构造方法会将传入的String
对象的value
数组和hash
值赋值给新创建的String
对象。
3. 有参构造方法:参数为char
数组
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
该构造方法会将传入的char
数组复制到新的char
数组中,并赋值给value
。这样做是为了避免外部对传入的char
数组进行修改而影响String
对象的内容。
4. 有参构造方法:参数为char
数组、起始位置和长度
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);
}
该构造方法会从传入的char
数组的指定起始位置offset
开始,复制count
个字符到新的char
数组中,并赋值给value
。同时会对传入的参数进行合法性检查,如果参数不合法则会抛出StringIndexOutOfBoundsException
异常。
(三)常用方法
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;
}
equals
方法用于比较两个String
对象的内容是否相等。首先会比较两个对象的引用是否相同,如果相同则直接返回true
;然后会检查传入的对象是否为String
类型,如果是,则比较两个String
对象的长度和每个字符是否相同。
2. 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;
}
hashCode
方法用于计算String
对象的哈希码。如果hash
值为0且value
数组的长度大于0,则会重新计算哈希码并赋值给hash
。计算哈希码的公式为:h = 31 * h + val[i]
,其中31
是一个质数,这样可以减少哈希冲突的概率。
3. substring
方法
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
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
方法用于截取String
对象的子字符串。会对传入的起始位置和结束位置进行合法性检查,如果参数不合法则会抛出StringIndexOutOfBoundsException
异常。如果截取的子字符串就是原字符串本身,则直接返回原字符串;否则会创建一个新的String
对象来表示截取的子字符串。
4. concat
方法
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);
}
concat
方法用于将指定的字符串连接到原字符串的末尾。如果指定的字符串长度为0,则直接返回原字符串;否则会创建一个新的char
数组,将原字符串和指定字符串的内容复制到新数组中,并创建一个新的String
对象来表示连接后的字符串。
5. replace
方法
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
replace
方法用于将原字符串中的所有指定字符oldChar
替换为新字符newChar
。如果oldChar
和newChar
相同,则直接返回原字符串;否则会遍历原字符串,找到第一个oldChar
的位置,然后创建一个新的char
数组,将原字符串中oldChar
之前的字符复制到新数组中,之后将oldChar
替换为newChar
并复制到新数组中,最后创建一个新的String
对象来表示替换后的字符串。
三、String对象的创建方式
(一)字面量方式
String s1 = "abc";
这种方式会先在字符串常量池中查找是否存在“abc”对象,如果存在则直接返回该对象的引用;如果不存在,则在字符串常量池中创建“abc”对象并返回其引用。
(二)new
关键字方式
String s2 = new String("abc");
这种方式会在堆内存中创建一个新的String
对象,同时会在字符串常量池中检查是否存在“abc”对象,如果不存在则会在字符串常量池中创建“abc”对象。最终s2
指向的是堆内存中的新对象。
(三)intern
方法手动入池
String s3 = new String("abc").intern();
这里先在堆内存中创建一个“abc”对象,然后调用intern
方法将该对象添加到字符串常量池中。如果常量池中已经存在“abc”对象,则s3
指向常量池中的对象;否则,将堆内存中的对象添加到常量池中并让s3
指向该对象。
四、String对象的拼接
(一)+
运算符拼接
String s1 = "a";
String s2 = "b";
String s3 = s1 + s2;
在编译期,+
运算符拼接实际上会被转换为StringBuilder
或StringBuffer
的append
方法。例如上面的代码在编译后相当于:
String s1 = "a";
String s2 = "b";
StringBuilder sb = new StringBuilder();
sb.append(s1);
sb.append(s2);
String s3 = sb.toString();
如果拼接的是常量字符串,则会在编译期进行优化,直接将常量拼接成一个新的字符串。例如:
String s4 = "a" + "b";
这里的s4
实际上等价于String s4 = "ab";
,会直接从字符串常量池中获取“ab”对象。
(二)concat
方法拼接
String s1 = "a";
String s2 = "b";
String s3 = s1.concat(s2);
concat
方法会创建一个新的String
对象来表示拼接后的字符串。与+
运算符拼接不同,concat
方法不会使用StringBuilder
或StringBuffer
。
(三)String.join
方法拼接
在JDK 8中,引入了String.join
方法来方便地拼接字符串。例如:
String[] words = {"Hello", "World"};
String result = String.join(" ", words);
这里的String.join
方法会使用指定的分隔符(这里是空格)将数组中的字符串拼接成一个新的字符串。
五、String类与其他相关类的比较
(一)String
与StringBuffer
、StringBuilder
的区别
1. 可变性
String
是不可变字符串,一旦创建其值不能被更改;而StringBuffer
和StringBuilder
是可变字符串,可以动态修改字符串的内容。
2. 线程安全性
StringBuffer
是线程安全的,其方法都使用了synchronized
关键字进行同步;StringBuilder
是非线程安全的,但性能比StringBuffer
更高。因此,在单线程环境下,优先使用StringBuilder
;在多线程环境下,需要使用StringBuffer
来保证线程安全。
3. 使用场景
如果字符串值不会改变,推荐使用String
;如果需要在单线程环境下频繁修改字符串,使用StringBuilder
是最佳选择;如果需要在多线程环境下安全地修改字符串,使用StringBuffer
。
(二)JDK版本对String
类的影响
- JDK 6及之前:
String
对象主要有四个成员变量:char
数组、偏移量offset
、字符数量count
、哈希值hash
。String
对象通过offset
和count
两个属性来定位char[]
数组,获取字符串。这种方式可能会导致内存泄漏。 - JDK 7 - JDK 8:
String
类中不再有offset
和count
两个变量,String
对象占用的内存稍微少了些,同时substring
方法也不再共享char[]
,从而解决了使用该方法可能导致的内存泄漏问题。 - JDK 9及之后:将
char[]
字段改为了byte[]
字段,并维护了一个新的属性coder
,用于标识字符串的编码格式。这是为了节约内存空间,因为大多数情况下字符串只包含单字节编码的字符,使用byte
数组可以减少内存占用。
综上所述,JDK 8中的String
类是一个功能强大且设计精巧的类,其不可变性、共享性等特性使得它在Java编程中得到了广泛的应用。同时,了解String
类的源码和内存结构,有助于我们更好地理解Java的内存管理机制和优化程序性能。