Java中字符串类主要包含3种:String、StringBuilder、StringBuffer,今天我们就从源码、性能和使用场景等角度来深入分析Java中的字符串类。
1. String
String类提供了构造和管理字符串的各种基本操作,Java中所有类似"WSYN"的字面值都是String类的实例。以下是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
它是典型的Immutable(不可修改的)类,被声明为final class,这意味着String类不能被继承,并且所有属性也都是final的。由于String类是不可变性,类似拼接、裁剪字符串的操作都会产生新的String对象,所以字符串操作不当可能会产生大量临时字符串。JDK 8以前String类是使用char(字符)数组来存储字符串的,JDK 9以后改为byte(字节)数组存储。
1.1 常量池
在分析常量池前先看下java的内存区域:
在Java的内存区域中有三种常量池,分别是:Class常量池、运行时常量池和字符串常量池。
1.1.1 Class常量池
每一个Java文件被java编译器编译后都会生成一个Class字节码文件,Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有常量池,用于存放编译期生成的各种字面量和符号引用。字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
- 类和接口的全限定名
- 字段名称和描述符
- 方法名称和描述符
每个类都有一个Class常量池。
1.1.2 运行时常量池
运行时常量池存在于内存中的方法区。Class常量池被加载到内存之后会存放在运行时常量池中。除了保存Class文件中的符号引用外,还会将符号引用转为直接引用,并存储在运行时常量池中。
运行时常量池相对于Class常量池另一个特性就是动态性。除了在编译期产生的Class文件中常量池的内容会进入运行时常量池,运行期间也可能产生新的常量放入池中,如String类的intern()方法。
JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
1.1.3 字符串常量池
字符串常量池有两种使用方式:
(1)直接使用双引号声明出来的String对象会直接存储在常量池中;
String str = "wsyn";
(2)如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中(JDK 7后将字符串在堆的引用地址放入常量池)。
String str2 = new String("wsyn");
str2 = str2.intern();
Java中创建字符串的两种方式:
(1)直接使用双引号声明一个字符串时,JVM首先查找字符串常量池是否存在该字符串,若存在则直接返回该字符串的引用地址;若不存在则先在字符串常量池实例化该字符串,再返回引用地址。
String str = "wsyn";
(2)通过new关键字新建一个字符串时,会在堆中重新new一块内存,用于存储字符串信息,同时会在栈中创建对堆的地址引用(JDK 7后字符串堆的地址引用从栈中转到字符串常量池)。
String str2 = new String("wsyn");
字符串常量池的位置
JDK7以后字符串常量池从方法区的永久代中移入到堆中。因为方法区的内存空间太小且不方便扩展,而堆的内存空间比较大且扩展方便。
字符串常量池的内部结构
- 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。
- 在JDK1.6中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;
- 在JDK1.7中,StringTable的长度可以通过参数指定:
-XX:StringTableSize=11111
1.3 == 和 equals区别以及hash冲突
- ==是比较两个字符串的地址引用是否一致。
- 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;
}
那么问题来了,是否可以通过hashCode来比较两个字符串是否一致?
答案是否定的,两个字符串==或者equals,他们的hashCode肯定是一样的,反之不然。先看下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的计算方式为:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],s[n] 是字符串的第 n个字符,n 是字符串的长度,^ 表示求幂。由此可知不同字符串有可能会得出相同的hashCode,虽然概率很低。
2. StringBuffer和StringBuilder
StringBuffer和StringBuilder底层都是利用可修改的char字符数组(JDK 9以后改为byte字节数组 ),为了实现修改字符序列的目的;同时他们都继承了AbstractStringBuilder,区别仅在于最终的方法是否加了synchronized。
那么这个内部数组的长度为多大合适呢?目前,内部数组长度为构建时初始字符串+16,这就意味着如果没有输入初始字符串,那么初始长度就是16。如果字符串长度值这个长度,就会进行扩容,重新创建新的足够大的数组,进行arraycopy,抛弃掉原来的数组,会产生多重开销。所以在我们能够预计字符串长度,可以指定合适大小。
2.1 StringBuffer
StringBuffer是为了解决上述提到的String类拼接产生太多中间对象的问题而提供的一个类,可以通过append或者add方法,把字符串添加到已有序列的末尾或指定位置。
StringBuffer本质是一个线程安全的可修改序列,它的线程安全是通过把各种修改数据的方法都加上synchronized关键字实现的,由此也带来了额外的性能开销,所以非线程安全的需要,一般不推荐使用。
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
/**
* A cache of the last value returned by toString. Cleared
* whenever the StringBuffer is modified.
*/
private transient char[] toStringCache;
/** use serialVersionUID from JDK 1.0.2 for interoperability */
static final long serialVersionUID = 3388685877147921107L;
public StringBuffer() {
super(16);
}
public StringBuffer(int capacity) {
super(capacity);
}
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
public StringBuffer(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
@Override
public synchronized int length() {
return count;
}
@Override
public synchronized int capacity() {
return value.length;
}
@Override
public synchronized void ensureCapacity(int minimumCapacity) {
super.ensureCapacity(minimumCapacity);
}
@Override
public synchronized void trimToSize() {
super.trimToSize();
}
@Override
public synchronized void setLength(int newLength) {
toStringCache = null;
super.setLength(newLength);
}
@Override
public synchronized char charAt(int index) {
if ((index < 0) || (index >= count))
throw new StringIndexOutOfBoundsException(index);
return value[index];
}
.
.
.
.
}
2.2 StringBuilder
StringBuilder 是 JDK 5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
/** use serialVersionUID for interoperability */
static final long serialVersionUID = 4383685877147921099L;
public StringBuilder() {
super(16);
}
public StringBuilder(int capacity) {
super(capacity);
}
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
public StringBuilder(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
.
.
.
.
}