String和StringBuffer在字符串拼接性能上的比较是Java技术面试的常见问题,在很多书籍和文章中只是简单的说:“String是不可变类型,StringBuffer是可变类型;所以在字符串拼接时StringBuffer的效率要远高于String。”事实真的是这样吗?
1. 在只有字符串常量参与拼接的情况下,String的效率要远高于StringBuffer
package test;
public class StringTest {
public static void main(String[] args) {
long time1 = System.nanoTime();
String hql = " select ui.real_name borrowerName,u.mobile mobile, u.email email,u.id borrowerId, " +
" l.loan_title loanTitle,l.open_amount openAmount,l.contact_amount contactAmount," +
" l.interest interest,l.loan_months loanMonths,p.name productName,l.loan_status loanStatus," +
" l.borrow_type borrowType, l.repayment_type repaymentType,l.id id,due.leaveAmount leaveAmount," +
" due.leaveNum leaveNum,c.channel_zn_name channelName" +
" from loan l left join user_info ui on l.borrower_id=ui.user_id" +
" left join user u on l.borrower_id=u.id" +
" left join product p on l.product_id=p.id" +
" left join channel c on c.channel_en_name=u.utm_source" +
" right join (select brr.loan_id loanId, sum(brr.repay_principal_interest) leaveAmount,count(id) leaveNum " +
" from borrow_repay_record brr where brr.status=:status or DATE_FORMAT(brr.repay_date,'%y-%m-%d') >= DATE_FORMAT(:leave_now,'%y-%m-%d') " +
" GROUP BY brr.loan_id ) due on due.loanId=l.id " +
" where l.loan_status=:loan_status ";
long time2 = System.nanoTime();
StringBuffer sb = new StringBuffer();
sb.append("select ui.real_name borrowerName,u.mobile mobile, u.email email,u.id borrowerId,")
.append("l.loan_title loanTitle,l.open_amount openAmount,l.contact_amount contactAmount," )
.append("l.interest interest,l.loan_months loanMonths,p.name productName,l.loan_status loanStatus," )
.append("l.borrow_type borrowType, l.repayment_type repaymentType,l.id id,due.leaveAmount leaveAmount," )
.append("due.leaveNum leaveNum,c.channel_zn_name channelName" )
.append("from loan l left join user_info ui on l.borrower_id=ui.user_id" )
.append("left join user u on l.borrower_id=u.id" )
.append("left join product p on l.product_id=p.id" )
.append("left join channel c on c.channel_en_name=u.utm_source" )
.append("right join (select brr.loan_id loanId, sum(brr.repay_principal_interest) leaveAmount,count(id) leaveNum")
.append("from borrow_repay_record brr where brr.status=:status or DATE_FORMAT(brr.repay_date,'%y-%m-%d') >= DATE_FORMAT(:leave_now,'%y-%m-%d') ")
.append("GROUP BY brr.loan_id ) due on due.loanId=l.id")
.append("where l.loan_status=:loan_status");
long time3 = System.nanoTime();
System.out.println(time2-time1);
System.out.println(time3-time2);
}
}
运行结果
8397
56915
为什么会出现String的效率远高于StringBuffer的情况?这不是和书上说的相反了吗?
实际上这里是因为Java在编译java文件到class文件时,进行了优化,查看class文件:
// Compiled from StringTest.java (version 1.6 : 50.0, super bit)
public class test.StringTest {
// Method descriptor #6 ()V
// Stack: 1, Locals: 1
public StringTest();
0 aload_0 [this]
1 invokespecial java.lang.Object() [8]
4 return
Line numbers:
[pc: 0, line: 3]
Local variable table:
[pc: 0, pc: 5] local: this index: 0 type: test.StringTest
// Method descriptor #15 ([Ljava/lang/String;)V
// Stack: 5, Locals: 9
public static void main(java.lang.String[] args);
0 invokestatic java.lang.System.nanoTime() : long [16]
3 lstore_1 [time1]
4 ldc <String " select ui.real_name borrowerName,u.mobile mobile, u.email email,u.id borrowerId, l.loan_title loanTitle,l.open_amount openAmount,l.contact_amount contactAmount, l.interest interest,l.loan_months loanMonths,p.name productName,l.loan_status loanStatus, l.borrow_type borrowType, l.repayment_type repaymentType,l.id id,due.leaveAmount leaveAmount, due.leaveNum leaveNum,c.channel_zn_name channelName from loan l left join user_info ui on l.borrower_id=ui.user_id left join user u on l.borrower_id=u.id left join product p on l.product_id=p.id left join channel c on c.channel_en_name=u.utm_source right join (select brr.loan_id loanId, sum(brr.repay_principal_interest) leaveAmount,count(id) leaveNum from borrow_repay_record brr where brr.status=:status or DATE_FORMAT(brr.repay_date,'%y-%m-%d') >= DATE_FORMAT(:leave_now,'%y-%m-%d') GROUP BY brr.loan_id ) due on due.loanId=l.id where l.loan_status=:loan_status "> [22]
6 astore_3 [hql]
7 invokestatic java.lang.System.nanoTime() : long [16]
10 lstore 4 [time2]
12 new java.lang.StringBuffer [24]
15 dup
16 invokespecial java.lang.StringBuffer() [26]
19 astore 6 [sb]
21 aload 6 [sb]
23 ldc <String "select ui.real_name borrowerName,u.mobile mobile, u.email email,u.id borrowerId,"> [27]
25 invokevirtual java.lang.StringBuffer.append(java.lang.String) : java.lang.StringBuffer [29]
28 ldc <String "l.loan_title loanTitle,l.open_amount openAmount,l.contact_amount contactAmount,"> [33]
30 invokevirtual java.lang.StringBuffer.append(java.lang.String) : java.lang.StringBuffer [29]
33 ldc <String "l.interest interest,l.loan_months loanMonths,p.name productName,l.loan_status loanStatus,"> [35]
35 invokevirtual java.lang.StringBuffer.append(java.lang.String) : java.lang.StringBuffer [29]
38 ldc <String "l.borrow_type borrowType, l.repayment_type repaymentType,l.id id,due.leaveAmount leaveAmount,"> [37]
40 invokevirtual java.lang.StringBuffer.append(java.lang.String) : java.lang.StringBuffer [29]
43 ldc <String "due.leaveNum leaveNum,c.channel_zn_name channelName"> [39]
45 invokevirtual java.lang.StringBuffer.append(java.lang.String) : java.lang.StringBuffer [29]
48 ldc <String "from loan l left join user_info ui on l.borrower_id=ui.user_id"> [41]
50 invokevirtual java.lang.StringBuffer.append(java.lang.String) : java.lang.StringBuffer [29]
53 ldc <String "left join user u on l.borrower_id=u.id"> [43]
55 invokevirtual java.lang.StringBuffer.append(java.lang.String) : java.lang.StringBuffer [29]
58 ldc <String "left join product p on l.product_id=p.id"> [45]
60 invokevirtual java.lang.StringBuffer.append(java.lang.String) : java.lang.StringBuffer [29]
63 ldc <String "left join channel c on c.channel_en_name=u.utm_source"> [47]
65 invokevirtual java.lang.StringBuffer.append(java.lang.String) : java.lang.StringBuffer [29]
68 ldc <String "right join (select brr.loan_id loanId, sum(brr.repay_principal_interest) leaveAmount,count(id) leaveNum"> [49]
70 invokevirtual java.lang.StringBuffer.append(java.lang.String) : java.lang.StringBuffer [29]
73 ldc <String "from borrow_repay_record brr where brr.status=:status or DATE_FORMAT(brr.repay_date,'%y-%m-%d') >= DATE_FORMAT(:leave_now,'%y-%m-%d') "> [51]
75 invokevirtual java.lang.StringBuffer.append(java.lang.String) : java.lang.StringBuffer [29]
78 ldc <String "GROUP BY brr.loan_id ) due on due.loanId=l.id"> [53]
80 invokevirtual java.lang.StringBuffer.append(java.lang.String) : java.lang.StringBuffer [29]
83 ldc <String "where l.loan_status=:loan_status"> [55]
85 invokevirtual java.lang.StringBuffer.append(java.lang.String) : java.lang.StringBuffer [29]
88 pop
89 invokestatic java.lang.System.nanoTime() : long [16]
92 lstore 7 [time3]
94 getstatic java.lang.System.out : java.io.PrintStream [57]
97 lload 4 [time2]
99 lload_1 [time1]
100 lsub
101 invokevirtual java.io.PrintStream.println(long) : void [61]
104 getstatic java.lang.System.out : java.io.PrintStream [57]
107 lload 7 [time3]
109 lload 4 [time2]
111 lsub
112 invokevirtual java.io.PrintStream.println(long) : void [61]
115 return
Line numbers:
[pc: 0, line: 6]
[pc: 4, line: 7]
[pc: 7, line: 21]
[pc: 12, line: 23]
[pc: 21, line: 24]
[pc: 28, line: 25]
[pc: 33, line: 26]
[pc: 38, line: 27]
[pc: 43, line: 28]
[pc: 48, line: 29]
[pc: 53, line: 30]
[pc: 58, line: 31]
[pc: 63, line: 32]
[pc: 68, line: 33]
[pc: 73, line: 34]
[pc: 78, line: 35]
[pc: 83, line: 36]
[pc: 89, line: 38]
[pc: 94, line: 40]
[pc: 104, line: 41]
[pc: 115, line: 42]
Local variable table:
[pc: 0, pc: 116] local: args index: 0 type: java.lang.String[]
[pc: 4, pc: 116] local: time1 index: 1 type: long
[pc: 7, pc: 116] local: hql index: 3 type: java.lang.String
[pc: 12, pc: 116] local: time2 index: 4 type: long
[pc: 21, pc: 116] local: sb index: 6 type: java.lang.StringBuffer
[pc: 94, pc: 116] local: time3 index: 7 type: long
}
在main方法的第四行很容易看出,编译时Java把多个字符串常量拼接成了一个长字符串,效率自然比使用StringBuffer高得多。
2. 有变量参与字符串拼接时,StringBuffer的效率要高于String
3. 对于有大量字符串常量参与拼接时,可以将大段的字符串常量先用String的+进行拼接,再和其他部分使用StringBuffer进行拼接。
package test;
public class StringTest2 {
public static void main(String[] args) {
long time1 = System.nanoTime();
String sql = " SELECT llr.id,llr.finance_plan_subpoint_id as financePlanSubPointId," +
" llr.loan_Id as loanId,llr.lend_amount as lendAmount,l.interest" +
" FROM loan_lender_record llr JOIN loan l ON llr.loan_id = l.id " +
" WHERE llr.finance_plan_subpoint_id IN( SELECT id " +
" FROM finance_plan_subpoint fps " +
" WHERE fps.finance_plan_id = " +
System.nanoTime() +
")";
long time2 = System.nanoTime();
StringBuffer sb = new StringBuffer();
sb.append(" SELECT llr.id,llr.finance_plan_subpoint_id as financePlanSubPointId,")
.append(" llr.loan_Id as loanId,llr.lend_amount as lendAmount,l.interest" )
.append(" FROM loan_lender_record llr JOIN loan l ON llr.loan_id = l.id " )
.append(" WHERE llr.finance_plan_subpoint_id IN( SELECT id " )
.append(" FROM finance_plan_subpoint fps " )
.append(" WHERE fps.finance_plan_id = " )
.append(System.nanoTime())
.append(")");
long time3 = System.nanoTime();
String tempSql = " SELECT llr.id,llr.finance_plan_subpoint_id as financePlanSubPointId," +
" llr.loan_Id as loanId,llr.lend_amount as lendAmount,l.interest" +
" FROM loan_lender_record llr JOIN loan l ON llr.loan_id = l.id " +
" WHERE llr.finance_plan_subpoint_id IN( SELECT id " +
" FROM finance_plan_subpoint fps " +
" WHERE fps.finance_plan_id = ";
StringBuffer sb1 = new StringBuffer(tempSql);
sb1.append(System.nanoTime()).append(")");
long time4 = System.nanoTime();
System.out.println(time2-time1);
System.out.println(time3-time2);
System.out.println(time4-time3);
}
}
运行结果:
52249
41520
8397
4. 使用StringBuffer拼接字符串时,适当的直接初始化大小可以提升效率
StringBuffer的底层是char数组,默认长度是16;当长度不足时按照“新长度=(原长度+1)*2” 来增大。
// StringBuffer增大长度的实现
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);
}
如果字符串拼接的结果比较大,在使用StringBuffer时就会多次调用此函数,影响效率。所以可以直接在初始化时,设置好StringBuffer的长度。
字符串拼接的编码原则:
1. 全部由字符串常量组成的字符串进行拼接, 直接使用String的+操作即可。
2. 当有字符串变量参与拼接时,应当使用StringBuffer进行拼接操作。
3. 对于既有很多字符串常量也有字符串变量参与拼接,可以将大段的字符串常量先用String的+进行拼接,再和其他部分使用StringBuffer进行拼接。
4. 使用StringBuffer时,如果可以预估出结果长度,应该设置StringBuffer的初始化长度。