在Java 中一共提供了String
、StringBuffer
、StringBuilder
用来表示和操作字符串,对于字符串的处理我们经常在项目中可以使用的到。了解它们底层的实现可以帮我们正确的使用这三个类,当然作为面试的高频题目,如果面试的时候如果面到了这个题目我们也就不怕了(~ . ~。
String
首先来讲String
类,可能很多人知道String
对象是内容不可变的,至于为什么是不可变的,我们通过源码来一探究竟。String
类被final
关键字修饰这也意味着String
类不可以被继承,其中实现了Serializable
,Comparable
,CharSequence
接口,说明String
类自身可以被序列化、并且具有排序以及其他的功能。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
上面的信息并不能说明String
类内容是不可变的,String
底层其实是一个char
类型的数组,有关String
类的所有操作其实都是对这个数组进行操作然后进行处理的,比如下面的charAt()
方法就是根据传入的下标返回数组中对应的字符。
/** The value is used for character storage. */
private final char value[];
..........
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
这个char
类型的数组也是被final
修饰的,因为被final
修饰的变量不可改变,这也是为什么String
类型的数据不可变。
当你定义一个String
类型的字符串时首先会检查在内存中是否存在该String
类型的数据,如果存在那么定义的这个字符串对象就会直接指向内存中的对象并返回一个引用,如果不存在,那么就在内存中开辟一块空间用来存储该对象,并返回一个该对象的引用。
当对字符串进行相关的操作时,如果字符串中的数据发生变化,那么都会返回一个新的字符串的引用,也就是说在内存中原来的对象并没有被改变。
StringBuilder
与StringBuffer
我们把StringBuffer
与 StringBuilder
放在一起讲述,这两个类都继承了相同的抽象类AbstractStringBuilder
,都实现了Serializable
与CharSequence
接口。
StringBuffer
与 StringBuilder
类 和String
类不同的是它们两个都是内容可变的字符串,因为它们都实现了AbstractStringBuilder
抽象类。在AbstractStringBuilder
抽象类中也定义了一个char
类型的数组,当这两个类进行字符串的操作时,都会改变这个字符数组,因为它不被final
修饰,所以它们不像String
对象一样一旦被定义就不可改变。
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
/**
* The value is used for character storage.
*/
char[] value;
数组的长度是不可改变的(这也是为什么数组比集合的效率要高),那么为什么StringBuffer
与 StringBuilder
的字符串却是可以改变的呢?
我们以StringBuilder
类中的append()
为例,当StringBuilder
对象调用append()
方法时都会调用父类(AbstractStringBuilder
)中的append()
方法,在AbstractStringBuilder
中进行append()
操作时都会执行ensureCapacityInternal()
方法进行内部容量担保操作,ensureCapacityInternal()
方法中调用了expandCapacity()
扩展字符数组容量的方法。在expandCapacity()
方法中实现了对原字符数组的扩容,为了大家能够充分理解源码的意思我在源码中添加了注释。
// StringBuilder 中的append 方法
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
//以下是AbstractStringBuilder 中的方法
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
// count 表示原字符数组中使用的字符数,len 表示要添加的字符的长度,相加后表示新字符串的长度
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
..............
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
//当新字符串的长度大于原分配字符数组的长度时,调用扩展数组容量的方法
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
................
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
//如果条件成立,给新字符数组的长度赋值为新字符串的长度,条件不成立时直接使用newCapacity 做为数组的长度
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
//调用Arrays 中的方法返回指定长度的字符数组,到这里原字符数组的长度就被扩展了
value = Arrays.copyOf(value, newCapacity);
}
我们已经结合了源码分析了StringBuffer
与 StringBuilder
的类知道为什么这两个类的字符串是可以被扩展的,以及扩展的过程实现。
一
那么这两个类有什么不同呢?我们也以append()
方法为例好了。其中最大的不同就是StringBuffer
中使用的是同步的方法,它保证在并发执行的过程中能够正确的执行,但是它同样也存在着效率方面的问题,所以我们要正确的使用StringBuffer
与 StringBuilder
。
//StringBuffer 中的append() 方法
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
//StringBuilder 中的append() 方法
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
在这里另附一道题目:String 重载”+”,有兴趣的也可以看一看。