Java 6 和 Java 8中String、StringBuf
一、String
String的创建机制
因为String
在Java中使用过于频繁,Java为了避免在系统中产生大量的String
对象,引入了字符串常量池的概念。
其运行机制是:
创建一个字符串时,首先检查池中是否有值相同的字符串对象(equals
决定),如果有则不需要创建而是直接从常量池中找到的该字符串对象的引用;
如果没有则新建一个字符串对象,返回该对象引用,并且将新创建的字符串对象放入池中
但是通过new方法创建的String
对象是不检查字符串池的,而是直接在堆区或栈区创建一个新的对象,也不会把对象放入池中。
//通过直接量赋值方式,放入字符串常量池
String str1 = "123";
//通过new方式赋值方式,不放入字符串常量池
String str2 = new String(“123”);
str1 == str2 // false
String str3 = "1" + "2" + "3";
str3 == str1 // true
//在编译期的时候,str3即被编译成"123"字符串,而此时常量池中已经存在该字符串,所以str3与str1是相等的
String的特性
不可变
String
对象一旦生成,则不能再对它的值进行改变,这里的不可变指这个字符串对象无法改变,而我们平时定义的字符串变量虽然可以改变,但是实质上它是改变了这个变量的引用,相当于将这个变量指向了另外一个字符串对象,而一开始的字符串对象还是没有变的。
不可变的主要作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅度提高系统性能。
为什么不可变
我们看String
的源码可以发现String
底层是采用字符数组(char[])来存储字符串值,该数组的定义如下
private final char value[];
这个数组定义为private final
,在java中数组也是对象,所以当String
对象一旦初始化完成,其内部变量value的引用就无法改变,顶多只能改变数组中元素的值,但是在看遍String
的所有方法后,发现String
中根本没有一个方法可以改变value这个char数组里面的元素,所以在String
初始化完成后即不可变。
虽然我们在编码过程中经常会调用String
的toLowerCase
,substring
等方法,但是实际上这是生成的一个新字符串对象,只是将变量的引用指向了这个新对象,而没有改变原有字符串对象的值。
StringBuffer 和 StringBuilder
StringBuffer
和StringBuilder
都实现了AbstractStringBuilder
抽象类,拥有几乎一致对外提供的调用接口;
其底层在内存中的存储方式与String
相同,都是采用char
数组存储数据,只是这个char
数组没被final
修饰,因此这个char数组的引用可以改变且该数组中的元素也可以改变,所以StringBuffer/StringBuilder
对象的值是可以改变的。
而StringBuffer/StringBuilder
在改变char
数组过程中是在该对象自身内部进行的,所以对象本身的引用还是同一个。因此定义一个StringBuffer/StringBuilder
变量,修改其值之后,其引用还是同一个,不会改变。
两者对象在构造过程中,首先按照默认大小申请一个字符数组,由于会不断加入新数据,当超过默认大小后,会创建一个更大的数组,并将原先的数组内容复制过来,再丢弃旧的数组。因此,对于较大对象的扩容会涉及大量的内存复制操作,如果能够预先评估大小,可提升性能。
唯一需要注意的是:StringBuffer
是线程安全的,但是StringBuilder
是线程不安全的。可参看Java标准类库的源代码,StringBuffer
类中方法定义前面都会有synchronized
关键字。为此,StringBuffer
的性能要远低于StringBuilder
。
应用场景
在字符串内容不经常发生变化的业务场景优先使用String类。例如:常量声明、少量的字符串拼接操作等。如果有大量的字符串内容拼接,避免使用String
与String
之间的" + "
操作,因为这样会产生大量无用的中间对象,耗费空间且执行效率低下(新建对象、回收对象花费大量时间)。
在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在多线程环境下,建议使用StringBuffer
,例如XML解析、HTTP参数解析与封装。
在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在单线程环境下,建议使用StringBuilder
,例如SQL语句拼装、JSON封装等。