字符串在程序中的使用率很高,每种程序语言都对字符串做了特殊处理。Java中的字符串是以对象存在的,且底层有多特性。
一、String的特性
String 最大的特性是不可变,底层用一个数组来存储对应的值:
private final byte[] value;
可以看到,变量用 final 进行修饰,来避免引用的修改,并用 private 来让别的类无法直接使用,这里只保证了引用的不可变,如何保证值的不可变呢?
其实,值的需要从实现上来保证,String 中将所有引起 value 改变的方法,都新生成一个对象,例如拼接,裁剪等操作,都会 copy 一份再进行操作。
甚至,类也用 final 进行修饰,确保没有子类继承 String,进而对 value 进行修改。
来看个例子,下面的代码执行后会输出 "aaa",还是 "bbb" ?
public static void main(String[] args) {
String s = "aaa";
change(s);
System.out.println(s);
}
public static void change(String s) {
s = "bbb";
}
这里的最终结果是会输出 "aaa",因为赋值的时候,不是改变了 s 的 value 值,而是改变了 s 的引用,而引用是方法的形参,改了并不影响调用方,所以输出的是 "aaa"。
二、常量池
为何 String 的设计上一定要不可变呢?
因为字符串常量池的存在,常量池的实现,是因为大多系统上用的字符串大体是相同的,因此把常用的字符串缓存起来,放到一个池子里面,这样就可以进行复用,避免频繁创建对象带来的损耗。
如果 String 是可变的,常量池便无法实现,如下,如果 s1 改变了 "ccc" 的值,会影响到 s2的使用。
常量池除了缓存常用的字符串,还提供了 inter() 方法来让业务方自己添加需要的值到 pool 里面。
这里要注意下,Java 6 因为把常量池放到了永久代里面(PermGen),所以不建议频繁使用 inter,否则容易引发 OOM,后续的版本将常量池放到了堆中管理,解决了该问题。
但手动调用 inter 还是有一定成本,不仅污染了业务代码,而且通常使用的时候无法预测哪些需要进行缓存,在 G1 GC 的 Intern 机制里,直接在 JVM 层面做了优化,来确保相同字符串的引用是相同的。
三、不可变的魅力
不可变的优点还有哪些呢?
首先,是增加了代码的可读性,代码很大程度上来说是用来维护的,这样的设计更能够传达本身所具有的性质;
其次,是进行约束,避免有意无意改变值带来的问题;
再者,天然支持并发场景,因为不可变,所以没有线程安全的问题;
最后,是性能方面,除了可以支持常量池这样的设计,值得一提的还有 hashCode 的值也可以在创建之初就进行缓存,也是因为该特性,所以 String 特别适合做 Map 的 key 来使用。
四、StringBuilder 和 StringBuffer
虽然对常用的字符串进行了优化,但业务上如果字符串操作太多,也会产生太多的中间对象。 StringBuilder 和 StringBuffer 的设计便是解决这类问题。
它们都继承了AbstractStringBuilder,类里面有个可变的成员变量 byte[] value。
两者的区别在于 StringBuffer 的方法上都加了 synchronized 来支持多线程的场景。
五、应用
业务上,对字符串的操作较少的,可以用 String 类,例如一些常量的使用,或者简单的存储业务需要的值;
操作频繁,包括裁剪,拼接,替换等,并且有线程安全需求的,需要使用 StringBuffer,例如网关等场景;
操作频繁,但没有线程安全的要求,就使用 StringBuilder。