String
和
StringBuffer
之概览
非可变对象一旦创建之后就不能再被改变,可变对象则可以在创建之后被改变。
String
对象是非可变对象,
StringBuffer
对象则是可变对象。为获得更佳的性能你需要根据实际情况小心谨慎地选择到底使用这两者中的某一个。下面的话题会作详细的阐述。(注意:这个章节假设读者已经具备
Java
的
String
和
StringBuffer
的相关基础知识。)
创建字符串的较佳途径
你可以按照以下方式创建字符串对象:
1. String s1 = "hello";
String s2 = "hello";
2. String s3 = new String("hello");
String s4 = new String("hello");
上面哪种方式会带来更好的性能呢?下面的代码片断用来测量二者之间的区别。
StringTest1.java
package com.performance.string;
/** This class shows the time taken for creation of
* String literals and String objects.
*/
public class StringTest1 {
public static void main(String[] args){
// create String literals
long startTime = System.currentTimeMillis();
for(int i=0;i<50000;i++){
String s1 = "hello";
String s2 = "hello";
}
long endTime = System.currentTimeMillis();
System.out.println("Time taken for creation of String literals : "
+ (endTime - startTime) + " milli seconds" );
// create String objects using 'new' keyword
long startTime1 = System.currentTimeMillis();
for(int i=0;i<50000;i++){
String s3 = new String("hello");
String s4 = new String("hello");
}
long endTime1 = System.currentTimeMillis();
System.out.println("Time taken for creation of String objects : "
+ (endTime1 - startTime1)+" milli seconds");
}
}
这段代码的输出:
Time taken for creation of String literals : 0 milli seconds
Time taken for creation of String objects : 170 milli seconds
JVM
是怎样处理字符串的呢?
Java
虚拟机会维护一个内部的滞留字符串对象的列表(唯一字符串的池)来避免在堆内存中产生重复的
String
对象。当
JVM
从
class
文件里加载字符串字面量并执行的时候,它会先检查一下当前的字符串是否已经存在于滞留字符串列表,如果已经存在,那就不会再创建一个新的
String
对象而是将引用指向已经存在的
String
对象,
JVM
会在内部为字符串字面量作这种检查,但并不会为通过
new
关键字创建的
String
对象作这种检查。当然你可以明确地使用
String.intern()
方法强制
JVM
为通过
new
关键字创建的
String
对象作这样的检查。这样可以强制
JVM
检查内部列表而使用已有的
String
对象。
所以结论是,
JVM
会内在地为字符串字面量维护一些唯一的
String
对象,程序员不需要为字符串字面量而发愁,但是可能会被一些通过
new
关键字创建的
String
对象而困扰,不过他们可以使用
intern()
方法来避免在堆内存上创建重复的
String
对象来改善
Java
的运行性能。下一小节会向大家展示更多的信息。
下图展示了未使用
intern()
方法来创建字符串的情况。
string_creating_without_intern() method
你可以自己使用
==
操作符和
String.equals()
方法来编码测试上面提到的区别。
==
操作符会返回
true
如果一些引用指向一个相同的对象但不会判断
String
对象的内容是否相同;
String.equals()
方法会返回
true
如果被操作的
String
对象的内容相同。对于上面的代码会有
s1==s2
,因为
s1
和
s2
两个引用指向同一个对象,对于上面的代码,
s3.equals(s4)
会返回
true
因为两个对象的内容都一样为
”hello”
。你可以从上图看出这种机制。在这里有三个独立的包含了相同的内容(
”hello”
)的对象,实际上我们不需要这么三个独立的对象
——
因为要运行它们的话既浪费时间又浪费内存。
那么怎样才能确保
String
对象不会重复呢?下一个话题会涵盖对于内建
String
机制的兴趣。
滞留字符串的优化作用
同一个字符串对象被重复地创建是不必要的,
String.intern ()
方法可以避免这种情况。下图说明了
String.intern()
方法是如何工作的,
String.intern()
方法检查字符串对象的存在性,如果需要的字符串对象已经存在,那么它会将引用指向已经存在的字符串对象而不是重新创建一个。下图描绘了使用了
intern()
方法的字符串字面量和字符串对象的创建情况。
string_creating_with_intern() method
下面的例程帮助大家了解
String.intern()
方法的重要性。
StringTest2.java
package com.performance.string;
// This class shows the use of intern() method to improve performance
public class StringTest2 {
public static void main(String[] args){
// create String references like s1,s2,s3...so on..
String variables[] = new String[50000];
for( int i=0;i<variables.length;i++){
variables[i] = "s"+i;
}
// create String literals
long startTime0 = System.currentTimeMillis();
for(int i=0;i<variables.length;i++){
variables[i] = "hello";
}
long endTime0 = System.currentTimeMillis();
System.out.println("Time taken for creation of String literals : "
+ (endTime0 - startTime0) + " milli seconds" );
// create String objects using 'new' keyword
long startTime1 = System.currentTimeMillis();
for(int i=0;i<variables.length;i++){
variables[i] = new String("hello");
}
long endTime1 = System.currentTimeMillis();
System.out.println("Time taken for creation of String objects with 'new' key word : "
+ (endTime1 - startTime1)+" milli seconds");
// intern String objects with intern() method
long startTime2 = System.currentTimeMillis();
for(int i=0;i<variables.length;i++){
variables[i] = new String("hello");
variables[i] = variables[i].intern();
}
long endTime2 = System.currentTimeMillis();
System.out.println("Time taken for creation of String objects with intern(): "
+ (endTime2 - startTime2)+" milli seconds");
}
}
这是上面那段代码的输出结果:
Time taken for creation of String literals : 0 milli seconds
Time taken for creation of String objects with 'new' key word : 160 milli seconds
Time taken for creation of String objects with intern(): 60 milli seconds
连接字符串时候的优化技巧
你可以使用
+
操作符或者
String.concat()
或者
StringBuffer.append()
等办法来连接多个字符串,那一种办法具有最佳的性能呢?
如何作出选择取决于两种情景,第一种情景是需要连接的字符串是在编译期决定的还是在运行期决定的,第二种情景是你使用的是
StringBuffer
还是
String
。通常程序员会认为
StringBuffer.append()
方法会优于
+
操作符或
String.concat()
方法,但是在一些特定的情况下这个假想是不成立的。
1)
第一种情景:编译期决定相对于运行期决定
请看下面的
StringTest3.java
代码和输出结果。
package com.performance.string;
/** This class shows the time taken by string concatenation at compile time and run time.*/
public class StringTest3 {
public static void main(String[] args){
//Test the String Concatination
long startTime = System.currentTimeMillis();
for(int i=0;i<5000;i++){
String result = "This is"+ "testing the"+ "difference"+ "between"+
"String"+ "and"+ "StringBuffer";
}
long endTime = System.currentTimeMillis();
System.out.println("Time taken for string concatenation using + operator : "
+ (endTime - startTime)+ " milli seconds");
//Test the StringBuffer Concatination
long startTime1 = System.currentTimeMillis();
for(int i=0;i<5000;i++){
StringBuffer result = new StringBuffer();
result.append("This is");
result.append("testing the");
result.append("difference");
result.append("between");
result.append("String");
result.append("and");
result.append("StringBuffer");
}
long endTime1 = System.currentTimeMillis();
System.out.println("Time taken for String concatenation using StringBuffer : "
+ (endTime1 - startTime1)+ " milli seconds");
}
}
这是上面的代码的输出结果:
Time taken for String concatenation using + operator : 0 milli seconds
Time taken for String concatenation using StringBuffer : 50 milli seconds
很有趣地,
+
操作符居然比
StringBuffer.append()
方法要快,为什么呢?
这里编译器的优化起了关键作用,编译器像下面举例的那样简单地在编译期连接多个字符串。它使用编译期决定取代运行期决定,在你使用
new
关键字来创建
String
对象的时候也是如此。
编译前:
String result = "This is"+"testing the"+"difference"+"between"+"String"+"and"+"StringBuffer";
编译后:
String result = "This is testing the difference between String and StringBuffer";
这里
String
对象在编译期就决定了而
StringBuffer
对象是在运行期决定的。运行期决定需要额外的开销当字符串的值无法预先知道的时候,编译期决定作用于字符串的值可以预先知道的时候,下面是一个例子。
编译前:
public String getString(String str1,String str2) {
return str1+str2;
}
编译后:
return new StringBuffer().append(str1).append(str2).toString();
运行期决定需要更多的时间来运行。
2)
第二种情景:使用
StringBuffer
取代
String
看看下面的代码你会发现与情景一相反的结果
——
连接多个字符串的时候
StringBuffer
要比
String
快。
StringTest4.java
package com.performance.string;
/** This class shows the time taken by string concatenation
using + operator and StringBuffer */
public class StringTest4 {
public static void main(String[] args){
//Test the String Concatenation using + operator
long startTime = System.currentTimeMillis();
String result = "hello";
for(int i=0;i<1500;i++){
result += "hello";
}
long endTime = System.currentTimeMillis();
System.out.println("Time taken for string concatenation using + operator : "
+ (endTime - startTime)+ " milli seconds");
//Test the String Concatenation using StringBuffer
long startTime1 = System.currentTimeMillis();
StringBuffer result1 = new StringBuffer("hello");
for(int i=0;i<1500;i++){
result1.append("hello");
}
long endTime1 = System.currentTimeMillis();
System.out.println("Time taken for string concatenation using StringBuffer : "
+ (endTime1 - startTime1)+ " milli seconds");
}
}
这是上面的代码的输出结果:
Time taken for string concatenation using + operator : 280 milli seconds
Time taken for String concatenation using StringBuffer : 0 milli seconds
看得出
StringBuffer.append()
方法要比
+
操作符要快得多,为什么呢?
原因是两者都是在运行期决定字符串对象,但是
+
操作符使用不同于
StringBuffer.append()
的规则通过
String
和
StringBuffer
来完成字符串连接操作。(译注:什么样的规则呢?)
借助
StringBuffer
的初始化过程的优化技巧
你可以通过
StringBuffer
的构造函数来设定它的初始化容量,这样可以明显地提升性能。这里提到的构造函数是
StringBuffer(int length)
,
length
参数表示当前的
StringBuffer
能保持的字符数量。你也可以使用
ensureCapacity(int minimumcapacity)
方法在
StringBuffer
对象创建之后设置它的容量。首先我们看看
StringBuffer
的缺省行为,然后再找出一条更好的提升性能的途径。
StringBuffer
的缺省行为:
StringBuffer
在内部维护一个字符数组,当你使用缺省的构造函数来创建
StringBuffer
对象的时候,因为没有设置初始化字符长度,
StringBuffer
的容量被初始化为
16
个字符,也就是说缺省容量就是
16
个字符。当
StringBuffer
达到最大容量的时候,它会将自身容量增加到当前的
2
倍再加
2
,也就是(
2*
旧值
+2
)。
如果你使用缺省值,初始化之后接着往里面追加字符,在你追加到第
16
个字符的时候它会将容量增加到
34
(
2*16+2
),当追加到
34
个字符的时候就会将容量增加到
70
(
2*34+2
)。无论何事只要
StringBuffer
到达它的最大容量它就不得不创建一个新的字符数组然后重新将旧字符和新字符都拷贝一遍
——
这也太昂贵了点。所以总是给
StringBuffer
设置一个合理的初始化容量值是错不了的,这样会带来立竿见影的性能增益。
我利用两个
StringBuffer
重新测试了上面的
StringTest4.java
代码,一个未使用初始化容量值而另一个使用了。这次我追加了
50000
个
’hello’
对象没有使用
+
操作符。区别是我使用
StringBuffer(250000)
的构造函数来初始化第二个
StringBuffer
了。
输出结果如下:
Time taken for String concatenation using StringBuffer with out setting size: 280 milli seconds
Time taken for String concatenation using StringBuffer with setting size: 0 milli seconds
StringBuffer
初始化过程的调整的作用由此可见一斑。所以,使用一个合适的容量值来初始化
StringBuffer
永远都是一个最佳的建议。
关键点
1.
无论何时只要可能的话使用字符串字面量来常见字符串而不是使用
new
关键字来创建字符串。
2.
无论何时当你要使用
new
关键字来创建很多内容重复的字符串的话,请使用
String.intern()
方法。
3. +
操作符会为字符串连接提供最佳的性能
——
当字符串是在编译期决定的时候。
4.
如果字符串在运行期决定,使用一个合适的初期容量值初始化的
StringBuffer
会为字符串连接提供最佳的性能。