在Java世界中,除了基本数据类型和包装数据类型,最常用的要数字符串类型,也就是String类型,其他还有StringBuilder、StringBuffer,他们堪称字符串三兄弟。
1、String
不可变对象
首先重要的一点:String是不可变对象,通过源码我们发现在String类的定义中加入了final关键字,使得String变成了不可变对象,具体如下面源码所示:
public final class String //final关键字用在类上,则该类不能被继承
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; //字符串的底层数据结构是字符数组,同样的字符数组被final修饰,一旦初始化完成,则字符数组的内存地址指向不能改变
所以,每次对String对象重新赋值时,都是指向了一块新的内存空间,这也就是String是不可变对象。
字符串常量池
Java为了避免在一个系统中产生大量的String对象,引入了字符串常量池。
创建一个字符串时,首先会检查池中是否有值相同的字符串对象,如果有就直接返回引用,不会创建字符串对象;如果没有则新建字符串对象,返回对象引用,并且将新创建的对象放入池中。但是,通过new方法创建的String对象是不检查字符串常量池的,而是直接在堆中创建新对象,也不会把对象放入池中。上述原则只适用于直接给String对象引用赋值的情况。
String还提供了intern()方法。调用该方法时,如果字符串常量池中包括了一个等于此String对象的字符串(由equals方法确定),则返回池中的字符串的引用。否则,将此String对象添加到池中,并且返回此池中对象的引用。如下面一段代码的运行结果,可以用上面的描述进行解释。
public class TestString {
public static void main(String[] args) {
String str1 = "hello";//检查字符串常量池的
String str2 = new String("hello");//不检查字符串常量池的,直接在堆中生成对象
String str3 = "he" + "llo";//字符串的拼接,通过反编译,会发现,JVM对此进行了优化:
//通过StringBuilder进行拼接的,即StringBuilder的append的方法进行拼接,最后结果转化成String
//所以结果和Str1一样,也是先检查字符串常量池
System.out.println(str1 == str2);//结果是false,一个在字符串常量池,一个在堆中,地址不同
System.out.println(str2.intern() == str1);//调用intern(),结果是true
System.out.println(str1.equals(str2));//结果是true
System.out.println(str1 == str3);//结果是true
System.out.println(str2 == str3);//结果是false
System.out.println(str2.intern() == str3);//结果是true
}
}
在jdk 8以后,常量池从JVM的永久代(方法区的一种实现)转移到了jdk 8的堆中,避免了永久代被挤满。同时,在jdk 8 中永久代改成了元空间(方法区的另一种实现),并且元空间直接放到内存,而不是JVM内存中了。
2、StringBuilder和StringBuffer
通过源码,可以发现,二者都继承了AbstractStringBuilder类,如下面代码:
public final class StringBuilder //同样有final关键字,表明该类不能被继承
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
public final class StringBuffer //与StringBuilder一样,都继承了AbstractStringBuilder类
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
//这是AbstractStringBuilder类的定义
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;//看到底层也是通过字符数组进行存储,但与String不同,这里没有final修饰
与String进行对比,可以理解为,StringBuilder和StringBuffer是可变的字符串,通过拼接等操作,可以在初始地址上扩展字符串,而不是像String一样,开辟新的空间,重新指向。如下面的代码:
public class TestString {
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("he");
System.out.println(stringBuilder);//输出he
stringBuilder.append("llo");
System.out.println(stringBuilder);//输出hello
}
}
这里,我们知道StringBuilder和StringBuffer是可变长度的,而底层是通过字符数组实现的,那么这个就是一个可以扩容的数据结构,而且会有一个初始容量或者设置初始容量的大小,通过阅读源码,可以发现就是这样的:
1、默认初始容量
/**
* Constructs a string builder with no characters in it and an
* initial capacity of 16 characters.
*/
public StringBuilder() { //默认构造函数,初始容量是16
super(16);
}
/**
* Constructs a string builder with no characters in it and an
* initial capacity specified by the {@code capacity} argument.
*
* @param capacity the initial capacity.
* @throws NegativeArraySizeException if the {@code capacity}
* argument is less than {@code 0}.
*/
public StringBuilder(int capacity) { //也可以设置初始容量大小,一般,如果能预知容量大小时,//最好设置一个固定的容量,在满足使用需要的同时,避免自动扩容对性能的影响
super(capacity);
}
//二者都调用了父类的构造方法
/**
* Creates an AbstractStringBuilder of the specified capacity.
*/
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
2、扩容机制
上面通过源码,了解了默认容量大小,还有用户可以在构造函数中设置初始容量,在容量不足时,会触发扩容,一般通过append方法添加字符串时,会触发扩容,如下面源码:
public StringBuilder append(String str) {
super.append(str); //调用父类的append方法
return this;
}
//父类的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;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {//如果插入后的总长度大于已有空间(数组的总长度),则先扩容,再把已有的字符串通过Arrays.copyOf函数复制到扩容后的地址空间,让value指针重新指向扩容后的地址空间
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
private int newCapacity(int minCapacity) {//扩容,返回申请新的空间的长度
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;//长度是原来的2倍加2
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
上面主要针对StringBuilder进行讲解,StringBuffer基本和其一致,主要区别如下面源码所示:
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
//通过这两个方法的片段我们可以发现,StringBuffer方法上多了synchronized关键字的修饰,使得StringBuffer方法变成了同步方法,所以在高并发场景下,StringBuffer是线程安全的。
总结:
1.从是否可变的角度
String类中使用字符数组保存字符串,因为有“final”修饰符,所以String对象是不可变的。StringBuffer和StringBuilder都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,但没有“final”修饰符,所以两种对象都是可变的。
2.是否多线程安全
String中的对象是不可变的,也就可以理解为常量,所以是线程安全的。
AbstractStringBuilder是StringBuffer和StringBuilder的公共父类,定义了一些字符串的基本操作,如append、、indexOf等公共方法。
StringBuffer对方法加了同步锁(synchronized) ,所以是线程安全的。
StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
参考文档:http://blog.itpub.net/31543790/viewspace-2220506/
https://baijiahao.baidu.com/s?id=1629804867201303563&wfr=spider&for=pc