博主也就写了两年代码,但是在以往只知道 String 与 StringBuffer 的用法、区别,但这两天抽空去恶补了一下,发现了 StringBuider 的好处。下面就说说这三个之间的区别吧。
对象 | final | 可变 | 线程安全 |
---|---|---|---|
String | √ | × | √ |
StringBuffer | × | √ | √ |
StringBuilder | × | √ | × |
String:
String 是 Java 中重要的数据类型,但他并不是 Java 的基本数据类型。在 c 语言中,对字符串的处理最通常的做法是使用 char 数组,但这种方式的弊端是显而易见的,数组本身无法封装字符串操作所需的基本方法。而在 Java 语言中,String 对象可以认为是 char 数组的眼神和进一步封装。如下图展示了 Java 中 String 类的基本实现,他主要是有三部分组成:char 数组、偏移量和 String 的长度。char 数组表示 String 的内容,它是 String 对象所表示字符串的超集。String 的真实内容还需要由偏移量和长度在这个 char 数组中进行定位和截取。
在 Java 语言中,Java 的设计者对 String 对象进行了大量的优化,其主要表现在了一下三个方面,同时这也是 String 对象的三个基本特点:
- 不变性:String 对象一旦生成,则不能在对他进行改变。这个特性可以泛化成不变模式。不变模式的主要作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅提高系统性能。
- 针对常量池的优化:当两个 String 对象用友相同的值时,它们只引用常量池中的同一个拷贝。
- 类的 final 定义:作为 final 类的 String 对象在系统中不可能有任何自雷,这是对系统安全性的保护。同时对于 JDK1.5版本之前的环境中,使用 final 定义,有助于帮助虚拟机寻找机会,内联所有的 final 方法,从而提高系统效率。但这种方式在 JDK1.5以后效果并不明显。
StringBuffer与 StringBuider:
由于 String 对象是不可变对象,因此在需要对字符串进行修改操作时,String 对象总是会生成新的对象,所以其性能相对较差。为此,JDK 专门提供了用于创建和修改字符串的工具,这就是 StringBuffer 和 StringBuider 类。
1.String 常量的累加操作
在上文中提到,String 对象具有不变性,因此,一旦 String 对象实例生成,就不可能在被改变。因此下面代码会生成几个对象:
String str = "a" + "b" + "c" + "d";
首先,由“a”和“b”两个字符串生成“ab”对象,然后依次生成“abc”和“abcd”对象。所以从理论上说,这段代码的效率并不高。
因此,为了能高效的动态生成和构建字符串对象,就需要使用 StringBuffer 和 StringBuilder类。
StringBuilder str = new StringBuilder();
str.append("a");
str.append("b");
str.append("c");
str.append("d");
上例代码只生成了一个实例 str,并通过 StringBuilder 的 append() 方法向其中追加字符串,其效率应该媛媛要高于前者。
为验证上述结论,通过实验对比以上代码的执行速度,分别做五万次循环,发现前者耗时0ms,后者耗时15ms。
实际结果却与预期相反,那么借助反编译工具我们看一下第一段代码反编译之后的样子:
String str = "abcd";
从以上结果可以看到,对于常量字符串的累加,Java 在编译时就做了充分的优化。对于这些在编译时能确定取值的字符串操作,在编译时就进行了计算,因此,在运行时这段代码并没有如想象中那样生成大量的 String 实例。而使用 StringBuffer 的代码反编译后的结果和源代码完全一致。课件在运行时 StringBuffer 对象和 append() 方法都被如实刁艳红,所以第一段代码的效率才会如此之快。
2.String 变量的累加操作
如果在编译时无法确定字符串的取值,那么对这些变量字符串的累加 Java 又会做什么。考察一下代码:
String str1 = "a";
String str2 = "b";
String str3 = "c";
String str4 = "d";
String str = str1 + str2 + str3 + str4;
现在将每个字串都定义到变量中,然后进行累加。这样编译器便无法在运行时确定 str 变量的取值。同样将这段代码运行无万次,平均耗时16ms。这个性能与 StringBuilder 的性能几乎一样。通过反编译,得到:
String str1 = "a";
String str2 = "b";
String str3 = "c";
String str4 = "d";
String str = (new StringBuilder(String.valueOf(str1)).append(str2).append(str3).append(str4).toString());
可以看到对于变量字符串的累加,Java 也做了相应的优化操作,使用了 StringBuilder 对象来实现字符串的累加,所以这段代码的性能和直接使用 StringBuilder 类的性能几乎一样。
3.构建超大的 String 对象
由以上两点可知,在代码实现中直接对 String 对象做的累加操作会在编译时被优化,因此其性能比理论值好很多。但是仍然建议在代码实现中显示的使用 StringBuilder 或者 StringBuffer 对象来提升系统性能,而不是依靠编译器对程序进行优化。
下面看一个长字符串连接的例子:
A:
for(int i = 0; i < 10000; i++) {
str = str + i;
}
B:
for(int i = 0; i < 10000; i++) {
str = str.concat(String.valueOf(i));
}
C:
StringBuilder str = new StringBuilder();
for(int i = 0; i < 10000; i++) {
str.append(i);
}
以上三个代码段 A、B、C 都进行了长字符串的连接操作。其中代码段 A 使用字符串的加法。如前一点中所述,此操作会被优化为 StringBuilder 的邓家实现;代码段 B 使用了 String 的 concat 方法进行字符串的连接;代码段 C 直接使用了 StringBuilder 类。同时所有的代码段都设为1万次循环。在同等条件下,A 耗时1062ms,B 耗时360ms,C 耗时0ms。这说明直接使用 StringBuilder 实现执行时间不到1ms。和代码段 A 相比,速度快了1000倍。
这里自然就产生了一个问题,根据上文所说,代码段 A 也是使用 StringBuilder 实现,为何性能会如此不尽人意。通过反编译,看到:
for(int i = 0; i < CIRCLE; i++)
str = (new StringBuilder(String.valueOf(str))).append(i).toString();
以上反编译代码显示,虽然 String 的加法运行呗编译成 StringBuilder 的实现,但在这种情况下,编译器并没有做出足够聪明的判断,每次循环都生成了新的 StringBuilder 实例,从而大大降低了系统性能。这和代码段 C 始终只维护一个 StringBuilder 实例相比,自然相形见绌了。
这个例子表名:String 的加法操作虽然会被优化,但编译器显然不够聪明,因此对于 String 操作,类似于“+”和“+=”的运算符应该尽量少用。其次,String 的 concat() 方法效率远远高于“+”和“+=”运算符,但是又远远低于 StringBuilder 类。
4.StringBuilder 和 StringBuffer 的选择
StringBuilder 和 StringBuffer是一堆孪生兄弟,虽然在上文中几乎所有的实现都使用了 StringBuilder。但如果大家自己尝试使用 StringBuffer 替代,也会得到类似的结果。
他们都实现了 AbstractStringBuilder 抽象类,用友几乎相同的对外接口,两者最大不同在意 StringBuffer 对几乎所有的方法都做了同步,而 StringBuilder 并没有做任何同步。
性能对比参见以下代码,其中加粗部分只取其一行运行。分别对两者进行追加操作循环50万次。结果 StringBuffer 相对耗时172ms,StringBuilder 相对耗时125ms。可见,非同步的 StringBuilder 用友更高的效率。
5.容量参数
无论是 StringBuilder 或者 StringBuffer,在初始化时都可以设置一个容量参数,对应的构造函数如下:
public StringBuilder(int capacity)
public StringBuffer(int capacity)
在不指定容量参数时,默认是16个字节。此参数制定了 StringBuilder / StringBuffer的出事大小:
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
追加字符串时,如果需要容量超过实际 char 数组长度,则需要进行扩容。扩容函数在 AbstractStringBuilder 中定义如下:
void expandCapacity(int minimumCapacity) {
int newCapacity = (value.length + 1) * 2;
if(newCapacity < 0) {
newCapacity = Integer.MAX_VALUE;
} else if(minimumCapacity > newCapacity) {
newCapacity = minimumCapacity;
}
value = Arrays.copyOf(value, newCapacity);
}
可以看到,扩容策略是将原有的容量大小翻倍,一新的容量申请内容空间,建立新的 char 数组,然后将原数组中的内容复制到这个新的数组中。因此,对于大对象的扩容会涉及大量的内存复制操作。所以,如果能够与先评估 StringBuilder 的大小,能够有效地节省这些操作,从而提高系统的性能。
StringBuffer sb = new StringBuffer(5888890);
StringBuilder sb = new StringBuffer(5888890);
for(int i = 0; i < 500000; i++) {
sb.append(i);
}
两个实例选其一进行测试,发现,StringBuffer 相对耗时78ms,StringBuilder 相对耗时46ms。均远远小于没有指定容量参数时的172ms 和125ms。
最后,再说下他们的使用场景吧。
如果定义一个变量,初始化之后值不变,不需要频繁的使用 + 来拼接字符串时,那么就使用 String;
反之,需要使用 StringBuilder 或 StringBuffer;
当需要频繁的 append 拼接字符串时,在单线程情况下,使用 StringBuilder 效率更快,在多线程情况下,使用 StringBuffer 效率更快。
备注:如果需要频繁的使用 substring(int, int) 方法时,一个不小心可能就会内存泄漏,具体为什么呢,敬请期待下次文章!
博主扣扣: 博主微信: