对于这三个,我们首先能知道的就是String是不可变的,StringBuilder和StringBuffer是可变的,那么我们就先说说String,它为什么设计成不可变的以及怎么实现不可变的。
String为什么设计成不可变的?
我们其实能感觉到,字符串其实是我们开发过程中最常用的一种数据结构了,如果依赖于常规的对象创建方式,那么就会出现大量重复字符串值的对象,这会消耗大量空间,从而影响GC效率。
所以如果设计成不可变的情况下,同样一种值的多个对象的引用都会指向一个字符串对象,可以大大的减少堆内存,同时String中缓存了hash值,这也会对使用hash的地方提升很多的性能。
同时设计成不可变的情况下,它就是线程安全的,即便在其他线程修改了值,那么也是创建或者引用已存在的对象,而不是修改当前的值。同时它对于安全性也是十分有保障的,一个不可变的内容,我们认为是可信的,如果可以随意的更改它的值,就太不可信了。
String设计如何实现不可变的?
先看一下jdk1.8中的源码
public final class String
implements java.io.Serializable, Comparable, 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
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 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);
}
}
从源码中我们可以看到,存储字符串的是用final修饰的char数组,表示这个字符数组不可变,而substring和concat方法返回的其实都是new String()。
扩展
在Java9及以上的版本,存储字符串的结构增加了一种,是byte数组,这其实是做了一层优化。而为什么这么做呢?Java内部使用UTF-16进行编码,也就是说即便一个单个字符可以用一个字节标识,用UTF-16之后也是占用两个字节,这其实是非常浪费时间的,而很多情况下的字符串其实都是可以用LATIN-1(单字节编码方案,可以标识包含ASCLL在内的128个字符)进行编码。所以引入了一种**“Compact String”** 的概念。那么该如何区分什么时候用UTF-16什么时候用LATIN-1呢?
在String的类中定义了一个名为coder 的字段,用来保存字符串是用什么进行编码的,然后根据类型存储到不同的存储结构中,与之相关的indexOf方法就需要这个字段来决定去哪个数组中找到对应字符。
当new String()的时候是做了什么?只是创建了一个对象吗?
Java对象在JVM中存储是有一定结构的,也就是对象模型 ,也包含了两个信息,一个是对象头,存储一些运行时的信息,比如线程,锁标识之类的,另一部分就是元数据,就是一个指向类信息的指针,关于JVM这方便的知识,后面我会单独的进行撰写。
其实不管怎么样,我们new的时候都会在堆上创建一个对象,但是对于字符串确实有一点特殊情况的,这个特殊情况就是常量池中的字符串常量 ,这个字符串其实是类编译阶段就进入到类常量池中的,当类第一次被ClassLoader加载时候,会从类常量池进入到运行时常量池(1.8以后字符串常量池移动到了堆中,为了更好的管理对象,防止内存泄露)。而字符串常量池中存储了字符串的引用与对象,引用存在String Table中,而new String()出来的都是在堆上面的对象实例,它的引用就是引用的字符串常量池中的字符串引用。所以可以看出,如果字符串常量池中没有这个对象,那么就可能创建两个对象,一个是在堆实例中,一个在字符串常量池中,创建一个还是两个对象都是取决于字符串常量池中有没有这个对象。
intern
上面经常在说字符串常量池,对于字符串常量池最通俗的解释就是程序运行时可以知道结果的字符串,比如下面这段代码
public static void main(String[] args) {
String a = "abc";
String b = "def";
String c = "abc"+"def";
}
反编译的结果就是String c="abcdef";当两个常量使用+的时候,就会变成一种常量。而另一种变量相加的方式
public static void main(String[] args) {
String a = "abc";
String b = "def";
String c = a+b;
}
反编译的结果就是
String c = (new StringBuilder()).append(a).append(b).toString();
而这种计算出来的结果值是不会进入到常量池中的,同时,这样的字符串还经常会用到呢,怎么办?所以intern的作用就体现出来了。它的作用就是两个,一个是如果常量池没有这个字符串的话,就将这个值加入到字符串常量池中,第二个就是返回这个常量的引用。
再次扩展-->String是否有长度限制呢?
答案是是有的,而且还不一样,在编译期间的String的最大长度为65535,运行期间的最大长度为int的最大值2^31-1。这里面涉及到了Java虚拟机规范的问题,大致点说就是虚拟机中用一个CONSTANT_Utf8_info的结构表示字符串常量,结构如下:
CONSTANT_Utf8_info{
u1 tag;
u2 length;
u1 bytes[length];
}
其中U2标识2个字节的无符号数,一个字节8位,2个字节就是16位,所以最大值为2^16-1 = 65535。
StringBuilder和StringBuffer都是可变的,且StringBuffer是线程安全的
StringBuilder和StringBuffer都继承了AbstractStringBuilder这里面有两个属性
char[] value;
/**
* The count is the number of characters used.
*/
int count;
并且都没有被final修饰,说明就是可变的,那么看一下他们的append源码
public AbstractStringBuilder append(StringBuffer sb) {
if (sb == null)
return appendNull();
int len = sb.length();
ensureCapacityInternal(count + len);
sb.getChars(0, len, value, count);
count += len;
return this;
}
其实就是干了2件事扩容和放字符。
StringBuffer中重写了append方法
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
都加上了synchronized,说明这是一个线程安全的方法。