String 是不可变的,StringBuilder 和 StringBuffer 是可变的。
而StringBuffer 是线程安全的,而StringBuilder 是非线程安全的。
我们都知道 String 是不可变的,但是它是怎么实现的呢?
先来看一段 String 的源码(JDK 1.8):
public final class String implements java.io.Serializable, Comparable<String>,CharSequence {
/** The value is used for character storage. */
private final char value[];
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
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);
}
}
以上代码,其实就包含了 String 不可变的主要实现了。
1. String类被声明为final,这意味着它不能被继承。那么他里面的方法就是没办法被覆盖的。
2. 用final修饰字符串内容的char[ ] (从JDK 1.9开始,char [ ] 变成了 byte[ ] ),由于该数组被声明为final,一旦数组被初始化,就不能再指向其他数组。
3. String类没有提供用于修改字符串内容的公共方法。例如,没有提供用于追加、删除或修改字符的方法。如果需要对字符串进行修改,会创建一个新的String对象。
再然后,在他的一些方法中,如substring、concat等,在代码中如果有涉及到字符串的修改,也是通过newString()的方式新建了一个字符串。
所以,通过以上方式,使得一个字符串的内容,一旦被创建出来,就是不可以修改的了。
不可变对象是在完全创建后其内部状态保持不变的对象。这意味着,一旦对象被赋值给变量,我们既不能更新引用,也不能通过任何方式改变内部状态。
可是有人会有疑惑,String为什么不可变,我的代码中经常改变String的值啊,如下:
String s = "abcd";
s = s.concat("ef");
这样,操作,不就将原本的"abcd"的字符串改变成"abcdef"了么?
但是,虽然字符串内容看上去从"abcd"变成了“abcdef",但是实际上,我们得到的已经是一个新的字符串了。
如上图,在堆中重新创建了一个“ abcdef ”字符串,和 “ abcd ” 并不是同一个对象。
So , 一旦一个 String 对象在内存(堆)中被创建出来,他就无法被修改。而且,String 类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。
如果我们想要一个可修改的字符串,可以选择 StringBuffer 或者 StringBuilder 这两个代替 String。
在Java 9之前,字符串内部是由字符数组char[ ] 来表示的。
/** The walue is used for character storage. */
private final char value[];
由于Java内部使用UTF-16,每个char占据两个字节,即使某些字符可以用一个字节(LATIN-1)表示,但是也仍然会占用两个字节。所以,JDK 9就对他做了优化。
这就是Java 9引入了"Compact String"的概念:
每当我们创建一个字符串时,如果它的所有字符都可以用单个字节(Latin-1)表示,那么将会在内部使用字节数组来保存一半所需的空间,但是如果有一个字符需要超过8位来表示,Java将继续使用UTF-16与字符数组。
Latin1(又称ISO 8859-1)是一种字符编码格式,用于表示西欧语言,包括英语、法语、德语、西班牙语、葡萄牙语、意大利语等。它由国际标准化组织(ISO)定义,并涵盖了包括ASCII在内的128个字符。
Latin1编码使用单字节编码方案,也就是说每个字符只占用一个字节,其中第一位固定为0,后面的七位可以表示128个字符。这样,Latin1编码可以很方便地与ASCII兼容。
那么,问题来了,所有字符串操作时,它如何区分到底用Latin-1还是UTF-16表示呢?
为了解决这个问题,对String的内部实现进行了另一个更改。引入了一个名为coder的字段,用于保存这些信息。
/**
* The value is used for character storage.
*
* @implNote This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*
* Additionally, it is marked with (@link Stable] to trust the contents
* of the array. No other facility in JDK provides this functionality (yet).
* (@link Stablel is safe here, because value is never null.
*/
@Stable
private final byte[] value;
/**
* The identifier of the encoding used to encode the bytes in
* (@code value]. The supported values in this implementation are
*
* LATIN1
* UTF16
*
* @implNote This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*/
private final byte coder;
coder字段的取值可以是以下两种:
static final byte LATIN1 = 0;
static final byte UTF16 = 1;
在很多字符串的相关操作中都需要做一下判断,如:
public int indexOf(int ch, int fromIndex) {
return islatin1()
? StringLatin1.indexOf(value, ch, fromIndex)
:StringUTF16.index0f(value, ch, fromIndex);
}
private boolean isLatin1() {
return COMPACT_STRINGS && coder == LATIN1;
}
为什么要把String设计成不可变的呢? 有什么好处呢?
这个问题,困扰过很多人,甚至有人直接问过Java的创始人James Gosling。
在一次采访中James Gosling被问到什么时候应该使用不可变变量,他给出的回答是:
I would use an immutable whenever I can.
那么,他给出这个答案背后的原因是什么呢? 是基于哪些思考的呢?
其实,主要是从缓存、安全性、线程安全和性能等角度出发的。
字符串是使用最广泛的数据结构。大量的字符串的创建是非常耗费资源的,所以,Java提供了对字符串的缓存功能,可以大大的节省堆空间。
JVM中专门开辟了一部分空间来存储Java字符串,那就是字符串池。
通过字符串池,两个内容相同的字符串变量,可以从池中指向同一个字符串对象,从而节省了关键的内存资源。
String s ="abcd";
String s2 = s;
对于这个例子,s和s2都表示”abcd”,所以他们会指向字符串池中的同一个字符串对象:
但是,之所以可以这么做,主要是因为字符串的不变性。试想一下,如果字符串是可变的,我们一旦修改了s的内容,那必然导致s2的内容也被动的改变了,这显然不是我们想看到的。
字符串在Java应用程序中广泛用于存储敏感信息,如用户名、密码、连接url、网络连接等。JVM类加载器在加载类的时也广泛地使用它。
因此,保护String类对于提升整个应用程序的安全性至关重要。
当我们在程序中传递一个字符串的时候,如果这个字符串的内容是不可变的,那么我们就可以相信这个字符串中的内容。
但是,如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。
不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。
因此,一般来说,不可变对象可以在同时运行的多个线程之间共享。它们也是线程安全的,因为如果线程更改了值,那么将在字符串池中创建一个新的字符串,而不是修改相同的值。因此,字符串对于多线程来说是安全的。
由于字符串对象被广泛地用作数据结构,它们也被广泛地用于哈希实现,如HashMap、HashTable、HashSet等。在对这些散列实现进行提作时,经常调用hashCode() 方法。
不可变性保证了字符串的值不会改变。因此,hashCode0方法在String类中被重写,以方便缓存,这样在第一次hashCode调用期间计算和缓存散列,并从那时起返回相同的值。
在String类中,有以下代码:
private int hash;//this is used to cache hash code.
前面提到了的字符串池、hashcode缓存等,都是提升性能的体现。
因为字符串不可变,所以可以用字符串池缓存,可以大大节省堆内存。而且还可以提前对hashcode进行缓存,更加高效。
由于字符串是应用最广泛的数据结构,提高字符串的性能对提高整个应用程序的总体性能有相当大的影响。
使用 + 拼接字符串,其实只是Java提供的一个语法糖,那么,我们就来解一解这个语法糖,看看他的内部原理到底是如何实现的。
还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。
String wechat = "Hollis";
String introduce = "Chuang";
String hollis = wechat + "," + introduce;
我们把这些个代码反编译以下,会得到如下代码:
String wechat ="Hollis";
String introduce = "Chuang";
String hollis = (new Stringbuilder()).append(wechat).append(",").append(introduce).tostring();
通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。
那么也就是说,Java中的 + 对字符串的拼接,其实现原理是使用StringBuilderappend。
接下来我们看看 StringBuffer 和 StringBuilder的实现原理。
和String 类相似,StringBuilder 类也封装了一个字符数组,定义如下:
char [] value;
与String不同的是,它并不是final的,所依它也是可以修改的,另外,与String 不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:
int count;
其append源码如下:
public StringBuilder append(String str);
super.append(str);
return this;
该类继承了 ‘AbstractStringBuilder ’ 类,看其下 ‘ append ’ 方法:
public AbstractStringBuilder append(String str) {
if (str == nul1)
return appendNul1();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0,en, value, count);
count += len;
return this;
}
append会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。
StringBuffer 和 StringBuilder 类似,,最大的区别就是 StringBuffer 是线程安的,看一下 StringBuffer 的 ‘append’ 方法。
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
该方法使用 synchronized 进行声明,说明是一个线程安全的方法。而 StringBuilder 则不是线程安全的.。
前面我们分析过,其实使用+拼接字符串的实现原理也是使用的StringBuilder,那为什么不建议大家在for循环中使用呢?
//我们把以下代码反编译下:
long t1 = System.currentTimeMillis();
String str = "hollis";
for (int i = 0; i < 50000; i++) {
String s = String.valueOf(i);
str += s;
}
long t2 = System.currentTimeMillis();
System.out.println("+ cost:" + (t2 - t1));
反编译后代码如下:
long t1 = System.currentTimeMillis();
String str = "hollis";
for(int i = 0; i < 50000; i++) {
String s = String.value0f(i);
str = (new StringBuilder()).append(str).append(s).toString();
long t2 = System.currentTimeMillis();
System.out.println((new StringBuilder()).append("+ cost:").append(t2 - t1).toString());
}
我们可以看到,反编译后的代码,在 for 循环中,每次都是 new 了一 StringBuilder ,然后再把 String 转成 StringBuilder ,再进行 append。
而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费
所以,阿里巴巴Java开发手册建议: 循环体内,字符审的连接方式,使用 StringBuilder 的 append 方法进行扩展。而不要使用 + 。