从内存方面解释Java中String与StringBuilder的性能差异
以前经常在网上看到关于java字符串拼接等方面的讨论。看到有些java开发人员在给新手程序员的建议中类似如下写道:
不要使用+号拼接字符串,要使用stringbuffer或stringbuilder的append()方法来拼接字符串。
不过,用+号拼接字符串就真的那么令人讨厌,难道使用+号拼接字符串就没有一点可取之处吗?
通过查阅java api文档中关于string类的部分内容,我们可以看到如下片段:
“java 语言提供对字符串串联符号("+")以及将其他对象转换为字符串的特殊支持。字符串串联是通过 stringbuilder(或 stringbuffer)类及其 append 方法实现的。字符串转换是通过 tostring 方法实现的,该方法由 object 类定义,并可被 java中的所有类继承。”
这段话很明确地告诉我们,在java中使用+号拼接字符串,实际上使用的就是stringbuffer或stringbuilder及其append方法来实现的。
除了java api文档,我们还可以使用工具查看class类文件的字节码命令来得到上述答案。 例如代码:
public static void main(string[] args) { string a = "hello"; string b = " world"; string str = a + b + " !"; system.out.println(str); }
通过工具查看到其对应的字节码命令如下:
从字节码命令中,我们可以清楚地看到,我们编写的如下代码
string str = a + b + " !";
被编译器转换成了类似如下语句:
string str = new stringbuilder(string.valueof(a)).append(b).append(" !").tostring();
不仅如此,java的编译器也是一个比较聪明的编译器,当+号拼接的全部是字符串字面量时,java的编译器将会在编译时智能地将其转换为一个完整的字符串。例如:
public static void main(string[] args) { string str = "hello" + " world" + ", java!"; system.out.println(str); }
java编译器直接将这种全是字面量的字符串拼接,在编译时就转换为了一个完整的字符串。
就算+号拼接的字符串中存在变量,java编译器也会将最前面的字符串字面量合并为一个字符串。
public static void main(string[] args) { string java = ", java!"; string str = "hello" + " world" + java; system.out.println(str); }
从上述可知,对于类似string str = str1 + str2 + str3 + str4,这种将多个字符串一次性拼接的操作,使用+号来进行拼接是完全没有问题的。
在java中,string对象是不可变的(immutable)。在代码中,可以创建多个某一个string对象的别名。但是这些别名都是的引用是相同的。
比如s1和s2都是”droidyue.com”对象的别名,别名保存着到真实对象的引用。所以s1 = s2
string s1 = "droidyue.com"; string s2 = s1; system.out.println("s1 and s2 has the same reference =" + (s1 == s2));
并且在java中,唯一被重载的运算符就是字符串的拼接相关的。+,+=。除此之外,java设计者不允许重载其他的运算符。
在java中,唯一被重载的运算符就是字符串的拼接相关的。+,+=。除此之外,java设计者不允许重载其他的运算符。
众所周知,在java 1.4版本之前,字符串拼接可以使用stringbuffer,从java 1.5开始,我们可以使用stringbuilder来拼接字符串。stringbuffer和stringbuilder的主要区别在于:stringbuffer是线程安全的,适用于多线程操作字符串;stringbuilder是线程不安全的,适合单线程下操作字符串。不过,我们的大多数字符串拼接操作都是在单线程下进行的,因此使用stringbuilder有利于提高性能。
在java 1.4之前,编译器使用stringbuffer来处理+号拼接的字符串;从java 1.5开始,编译器大多数情况下都使用stringbuilder来处理+号拼接的字符串。
当我们在jdk 1.4的环境下编写代码时,对于上述这种一次性拼接多个字符串的情况,建议最好使用+号来处理。这样,当jdk 升级到1.5及以上版本时,编译器将会自动将其转换为stringbuilder来拼接字符串,从而提高字符串拼接效率。
当然,推荐使用+号拼接字符串也仅限于在一条语句中拼接多个字符串时使用。如果分散在多条语句中拼接一个字符串,仍然建议使用stringbuffer或stringbuilder。 例如:
public static void main(string[] args) { string java = ", java!"; string str = ""; str += "hello"; str += " world"; str += java; system.out.println(str); }
编译器编译后的字节命令如下:
从上面的图片中我们可以知道,每一条+号拼接语句,都创建了一个新的stringbuilder对象。这种情况在循环条件下表现得尤其明显,造成了相对较大的性能损耗。因此,在多条语句中拼接字符串,强烈建议使用stringbuffer或stringbuilder来处理。
关于使用stringbuilder带来的优化
此外,在使用stringbuffer或stringbuilder的时候,我们还可以使用如下方式,进一步提高性能(下面代码以stringbuilder为例,stringbuffer与此类似)。
1.预测最终获得的字符串的最大长度。
stringbuilder内部char数组的默认长度为16,当我们append追加字符串后超过此长度时,stringbuilder会扩大内部的数组容量以满足需要。在这个过程中,stringbuilder会创建一个新的较大容量的char数组,并将原数组中的数据复制到新数组中。如果我们能够大致预测到最终拼接得到的字符串的最大长度,就可以在创建stringbuilder对象时指定合适大小的初始容量。例如,我们需要拼接获得含有100个字母a的字符串。即可编写如下代码:
stringbuilder sb = new stringbuilder(100); for (int i = 0; i < 100; i++) { sb.append('a'); } system.out.println(sb);
请根据实际情况进行平衡,以创建适合初始容量的stringbuilder。
2.对于单个字符,尽可能地使用char类型,而不是string类型。
有些时候,我们需要在字符串后追加单个字符(例如:a),此时应尽可能地使用
sb.append('a');
而不是使用:
sb.append("a");