导读
在Java中数据类型分为基本数据类型与引用数据类型。其中String属于引用类型,也是最常见的一种类型。但是我们对于String了解多少呢?String对象的内存地址?如何创建String对象?并发影响?等等。
关于Java的String内存存储位置及源码解析文章推荐阅读:
Java Final修饰符存储位置,为什么String是不可变的?
Android必须知道的Java内存结构及堆栈区别
一、String
探究String类源码,JDK1.7中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
由以上的代码可以看出, 在Java中String类其实就是对char value[](字符数组)的封装。在JDK7中,有一个value变量,也就是value中的所有字符都是属于String这个对象的。另外,还有一个hash成员变量,是该String对象引用地址的哈希值的缓存。在Java中,数组也是对象,所以value也只是一个引用,它指向一个真正的数组对象。
即,执行了String s1 = “ABCabc”这句代码之后,应该是这样的:
String s1 = char value[] ='A' 'B' 'C' 'a' 'b' 'c'
由于源码中这个 value 变量是加了final 修饰符的。 也就是说在String类内部,一旦value这个值初始化了, 就不能被改变。所以可以认为String对象是不可变的了。即,String的实例一旦生成就不会再改变了。如果执行 String str=”kv”+”ill”+” “+”ans”; 就有四个字符串常量,最终由于String的不可变导致通过“+”产生了很多不必要的临时变量且不是线程安全的,这种情况下使用StringBuffer更好。 因为从JDK 1.5开始,带有字符串变量的连接操作 +,JVM内部采用的是StringBuilder来实现的,而之前这个操作是采用StringBuffer实现的。所以,String的+操作实际是通过StringBuilderr的append方法进行操作,然后又通过toString()操作重新赋值的。
此外,使用String不一定创建对象。因为在 java中对 String 对象有特殊对待,在堆区域给分成了两块,一块是 String constant pool(常量池),另一块用于存储普通对象及字符串对象。比如:
String a ="123";
String b ="123"; //值“123”在常量池中已有实例对象,可直接引用
String c = new String("123456");//首先值“123456”在常量池中创建一个对象实例,然后new String在堆中又创建一个由c指向引用对象地址。
a == b == true ;//值在常量池的对象实例都是“123”,即引用地址相同
因为JVM会先到常量池中查找有没有“123”这个对象实例,发现没有“123”,然后会创建新的对象实例置入常量池中。所以,变量b会直接在拿到常量池中的实例引用。
但是,使用new String,一定创建对象。在执行String a = new String(“123456”)的时候,首先到常量池查询实例的引用(若没有,则创建一个”123456”对象),然后再通过new关键字创建一个新的String实例,实际上创建了两个String对象。
二、StringBuffer
通过上面的分析,当字符串数据进行拼接时候,为了避免产生很多不必要的临时变量,提高效率,需要使用StringBuffer或StringBuilder。其中,StringBuffer 是线程安全的可变字符序列,适用于多线程场景。在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容,可将字符串缓冲区安全地用于多个线程。因为使用StringBuffer类时,每次都会对 StringBuffer 对象本身进行操作,所以不会生成新的对象并改变对象引用。为了搞清楚原理还得从源码从发。
StringBuffer.class 源码:
public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence
{
public StringBuffer() {
super(16);//定义一个长度为16的数组
}
public StringBuffer(int capacity) {
super(capacity);
}
public StringBuffer(String str) {
super(str.length() + 16);
append(str); //字符串拼接
}
}
首先,StringBuffer类跟String类一样定义成final形式,为了保证变量初始化后的引用对象不可以重新赋值,主要是为了“效率”和“安全性”的考虑,但是对象实例的成员变量的值是可变的,这一点与String完全不同,后面源码会讲到。其次,StringBuffer实现的接口Serializable的作用就是为了序列化,就不多说了。
继续看, append(str) 方法源码如何实现字符串拼接:
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
这个方法对于诠释StringBuffer的特性来说是相当的重要了。首先 append 用的修饰符是 synchronized,说明在操作时是线程安全的,而这一点 StringBuilder 就没有。其次,从 return this; 可以看出不管执行多少次的append(String)方法,都会对 StringBuffer 对象本身进行操作,不会像String的字符串拼接那样new String()创建新的对象。最后,从super.append(str);可以看出在StringBuffer里直接调用父类的append方法,对于该方法的具体代码是在父类中实现的。
接下来,就来看看继承的父类 AbstractStringBuilder.class 和实现的接口 CharSequence.class。
接口 CharSequence.class 源码:
public interface CharSequence {
int length();
char charAt(int index);
CharSequence subSequence(int start, int end);
public String toString();
}
AbstractStringBuilder.class 源码:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* 与String类一样,定义了一个char类型的数组存储值 ,但是没有加final,说明值可变。
*/
char value[];
int count;
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
public synchronized String toString() { //同步,线程安全
return new String(value, 0, count);//生成一个新的String对象
}
public AbstractStringBuilder append(String str) { //append方法实现
if (str == null) str = "null"; //非null判断
int len = str.length();
if (len == 0) return this; //非空判断
int newCount = count + len;
if (newCount > value.length)
expandCapacity(newCount); //这一步主要是数组扩容
str.getChars(0, len, value, count); //这一步得到新数组
count = newCount;
return this;
}
....其他方法省略...}
首先,父类加了Abstract修饰符说明是抽象类,关于抽象类与接口的Java知识请移步学习。从 append()方法实现中可以看出,对str做了非空判断,然后走一个数组扩容的方法 expandCapacity(new count); 和得到新数组的 str.getChars() 方法。
expandCapacity():
void expandCapacity(int minimumCapacity) {
int newCapacity = (value.length + 1) * 2;//首先定义一个是原容量的2倍大小的值
if (newCapacity < 0) {
newCapacity = Integer.MAX_VALUE;
} else if (minimumCapacity > newCapacity) {//这一步主要是判断,取最大的值做新的数组容量大小
newCapacity = minimumCapacity;
}
value = Arrays.copyOf(value, newCapacity);//最后进行扩容
}
str.getChars():
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) { //数组越界异常
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > count) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
//前面三个判断主要是为了安全验证。这一步才是重点:将在原来的数组上追加新数组
System.arraycopy(value, offset + srcBegin, dst, dstBegin,srcEnd - srcBegin);
}
StringBuffer主要操作有 append() 和 insert() 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。
例如:
String s = “start”;
//StringBuffer z = new StringBuffer(“start”);
StringBuffer z = new StringBuffer(s); // String转换为StringBuffer
z.append("le");//startle
z.insert(6,"t");//插入内容t 。startlet
如果 z 引用一个当前内容是“start”的字符串缓冲区对象,则此方法调用 z.append(“le”) 会使字符串缓冲区包含“startle”,而 z.insert(6, “t”) 将更改字符串缓冲区,使之包含“starlet”。
三、StringBuilder
StringBuilder是一个线程不安全的字符序列,是JDK5.0新增被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候,速度较StringBuffer要更快。StringBuffer和StringBuilder都是继承自AbstractStringBuilder的。
AbstractStringBuilder原理:
AbstractStringBuilder中采用一个char数组来保存需要append的字符串,char数组有一个初始大小,当append的字符串长度超过当前char数组容量时,则对char数组进行动态扩展,也即重新申请一段更大的内存空间,然后将当前char数组拷贝到新的位置,因为重新分配内存并拷贝的开销比较大,所以每次重新申请内存空间都是采用申请大于当前需要的内存空间的方式。
为什么说StringBuilder的效率最高呢?从前面的源码分析中可以看出,因为StringBuffer的append()方法都是被synchronized修饰了,所以它线程安全,但是效率自然就降低,仅此而已。
public synchronized StringBuffer append(Object paramObject)
{
super.append(String.valueOf(paramObject));
return this;
}
append()对比:
public StringBuilder append(char paramChar)
{
super.append(paramChar);
return this;
}
在操作用法上与StringBuffer基本相似。
四、总结
- 如果要操作少量的数据,用String;
- 单线程操作大量数据,用StringBuilder;
- 多线程操作大量数据,用StringBuffer;
- 不要使用String类的”+”来进行频繁的拼接,因为性能很差,应该使用StringBuffer或StringBuilder类;
- 为了性能更好,构造StringBuffer或StringBuilder时应指定它们的容量,默认构造的容量为16个字符;
- StringBuilder最好在方法内部来完成字符串拼接,因为是线程不安全的,所以用完以后可以丢弃。而StringBuffer主要用在全局变量中;