17. String与StringBuffer、StringBulider区别
17.1、可变与不可变
在java中提供三个类String、StringBuillder、StringBuffer来表示和操作字符串。字符串就是多个字符的集合。
String是内容不可变的字符串。String底层使用了一个不可变的字符数组(final char[])。
StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串(没有使用final来修饰),如下就是,可知这两种对象都是可变的。
最经典的字符串拼接的例子:
String str = “hello”;
str = str + “world”;
由于String类是不可变的,实际上会在堆内存中会生成“hello”,“world”,“helloworld”三个String对象。当str指向新的“helloworld”时,前两个对象变成垃圾对象。
使用StringBuilder或者StringBuffer 则可以直接拼接:
StringBuilder sb = new StringBuilder(); sb.apend(“a”).apend(“b”)
String、StringBuffer、StringBuilder三个类的继承关系如下图所示:
17.2、是否多线程安全
String中的对象是不可变的,也就可以理解为常量,显然线程安全。
AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。
StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
17.3、总结
最后,如果程序不是多线程的,那么使用StringBuilder效率高于StringBuffer。
StringBuffer的容量
首先是两个概念capacity和length,使用str.capacity()得到的时字符串的容量大小,而str.length()得到的时字符串的实际长度。
直接使用new StringBuffer(String str)时,capacity是str.length + 16
如果直接是new StringBuffer(),则capacity为16
扩容操作时,容量扩展的规则是把旧的容量(value的长度)2+2
所以第一次append时,小于16则不需扩展,如果大于16则会直接扩展到34(162+2),如果append后的字符串小于34,则容量就为34,如果append后的字符串大于34,则容量为append后的长度(此时capacity和length相等,下一次append势必会扩容)。
即若新的capacity的大小等于append后的长度,则容量为该capacity,如果在append之后,长度大于capacity,则继续使用append后的长度为容量。
String对象的不变性
1、String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。在Java中,被final修饰的类是不允许被继承的,并且该类中的成员方法都默认为final方法。
2、String类其实是通过char数组来保存字符串的。
String使用private final char value[]来实现字符串的存储,也就是说String对象创建之后,就不能再修改此对象中存储的字符串内容,就是因为如此,才说String类型是不可变的(immutable)。
3、对于改变字符串的操作,无论是sub操、concat还是replace操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。
在这里要永远记住一点:String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象。
字符串常量池
我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会在堆中创建一份,然后返回堆中的地址。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串。
Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
当调用String类的intern()方法时,若常量池中已经包含一个等于此String对象的字符串(用Object的equels方法确定),则返回池中的字符串,否则将此String对象添加到池中,并返回此String对象在常量池中的引用。比如:
String s1 = new String(“asd”);
s1 = s1.intern();
String s2 = “asd”;
s1 == s2; // true
其他
1、引用变量与对象:A aa;
这个语句声明一个类A的引用变量aa[我们常常称之为句柄],而对象一般通过new创建。所以aa仅仅是一个引用变量,它不是对象。
2、创建字符串的方式,创建字符串的方式归纳起来有两类:
(1)、使用""引号创建字符串;
(2)、使用new关键字创建字符串。结合上面例子,总结如下:
A、单独使用""引号创建的字符串都是常量,编译期就已经确定存储到StringPool中;
B、使用new String("")创建的对象会存储到heap中,是运行期新创建的;
new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址(注意,此时不需要从堆中复制到池中,否则,将使得堆中的字符串永远是池中的子集,导致浪费池的空间)!
C、使用只包含常量的字符串连接符如"aa"+ "aa"创建的也是常量,编译期就能确定,已经确定存储到StringPool中;
D、使用包含变量的字符串连接符如"aa"+ s1创建的对象是运行期才创建的,存储在heap中;
3、使用String不一定创建对象,使用new String,一定创建对象。
在执行到双引号包含字符串的语句时,如String a = "123",JVM会先到常量池里查找,如果有的话返回常量池里的这个实例的引用,否则的话创建一个新实例并置入常量池里。所以,当我们在使用诸如String str = "abc";的格式定义对象时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每次都创建一个新的对象。
关于final
String中的final用法和理解
final StringBuffer a = new StringBuffer("111");
final StringBuffer b = new StringBuffer("222");
a = b; // 此句编译不通过
final StringBuffer a = new StringBuffer("111");
a.append("222"); // 编译通过
可见,final修饰引用变量只对引用的"值"(即内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。