哈嘍,歡迎進來學習的小伙伴~
【學習背景】
本文會通過OpenJDK提供的Java性能測驗工具
JMH來測驗下String、StringBuilder及StringBuffer拼接字串的效率如何~
關于JMH的介紹及具體使用,我的這篇博文中有介紹:
Java–??面試官:LinkedList真的比ArrayList添加元素快????本文通過Open JDK JMH帶你揭開真相《?建議收藏?》
當然,除了驗證三者的字串拼接效率之外,還會對這三者的特性及常見面試問題進行分析和總結,希望加深自己對這三者的認知,分享出來,也希望能幫助到有需要的小伙伴~
進入正文~~
學習目錄
- 一、性能測驗
- 1.1 代碼實作
- 1.2 測驗結果
- 1.2.1 普通展示
- 1.2.2 圖形展示
- 1.3 結果分析
- 二、區別說明
- 2.1 String
- 2.1.1 String特性
- 2.1.2 String常用API
- 2.1.3 String常見面試題(附參考答案)
- 2.2 StringBuilder
- 2.2.1 StringBuilder特性
- 2.2.2 StringBuilder常用API
- 2.2.3 StringBuilder常見面試題(附參考答案)
- 2.3 StringBuffer
- 2.3.1 StringBuffer特性
- 2.3.2 StringBuffer常用API
- 2.3.3 StringBuffer常見面試題(附參考答案)
一、性能測驗
1.1 代碼實作
分別撰寫String、StringBuilder及StringBuffer的JMH基準單元測驗方法:
StringAppendJmhTest.java
package com.justin.java;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) //基準測驗型別:time/ops(每次呼叫的平均時間)
@OutputTimeUnit(TimeUnit.NANOSECONDS) //基準測驗結果的時間型別:微秒
@Warmup(iterations = 5) //預熱:5 輪
@Measurement(iterations = 5) //度量:測驗5輪
@Fork(3) //Fork出3個執行緒來測驗
@State(Scope.Thread) // 每個測驗執行緒分配1個實體
public class StringAppendJmhTest {
@Param({"2", "10", "100", "1000"})
private int count; //指定添加元素的不同個數,便于分析結果
@Setup(Level.Trial) // 初始化方法,在全部Benchmark運行之前進行
public void init() {
System.out.println("Start...");
}
public static void main(String[] args) throws RunnerException {
//1、啟動基準測驗:輸出普通檔案
// Options opt = new OptionsBuilder()
// .include(ArrayAndLinkedJmhTest.class.getSimpleName()) //要匯入的測驗類
// .output("C:\\Users\\Administrator\\Desktop\\StringAppendJmhTest.log") //輸出測驗結果的普通txt檔案
// .build();
//1、啟動基準測驗:輸出json結果檔案(用于查看可視化圖)
Options opt = new OptionsBuilder()
.include(StringAppendJmhTest.class.getSimpleName()) //要匯入的測驗類
.result("C:\\Users\\Administrator\\Desktop\\StringAppendJmhTest.json") //輸出測驗結果的json檔案
.resultFormat(ResultFormatType.JSON)//格式化json檔案
.build();
//2、執行測驗
new Runner(opt).run();
}
@Benchmark
public void stringAppendTest(Blackhole blackhole) {
String str = new String();
for (int i = 0; i < count; i++) {
str = str + "Justin";
}
blackhole.consume(str);
}
@Benchmark
public void stringBufferAppendTest(Blackhole blackhole) {
StringBuffer strBuffer = new StringBuffer();
for (int i = 0; i < count; i++) {
strBuffer.append("Justin");
}
blackhole.consume(strBuffer);
}
@Benchmark
public void stringBuilderAppendTest(Blackhole blackhole) {
StringBuilder strBuilder = new StringBuilder();
for (int i = 0; i < count; i++) {
strBuilder.append("Justin");
}
blackhole.consume(strBuilder);
}
@TearDown(Level.Trial) // 結束方法,在全部Benchmark運行之后進行
public void clear() {
System.out.println("End...");
}
}
運行main方法進行測驗~
1.2 測驗結果
1.2.1 普通展示
查看控制臺輸出的結果資訊,拉到最后查看最后幾行的Score指標如下:
Benchmark (count) Mode Cnt Score Error Units
StringAppendJmhTest.stringAppendTest 2 avgt 15 43.029 ± 4.440 ns/op
StringAppendJmhTest.stringAppendTest 10 avgt 15 212.911 ± 22.882 ns/op
StringAppendJmhTest.stringAppendTest 100 avgt 15 9262.168 ± 431.742 ns/op
StringAppendJmhTest.stringAppendTest 1000 avgt 15 830811.924 ± 38227.519 ns/op
StringAppendJmhTest.stringBufferAppendTest 2 avgt 15 35.546 ± 1.159 ns/op
StringAppendJmhTest.stringBufferAppendTest 10 avgt 15 167.670 ± 4.900 ns/op
StringAppendJmhTest.stringBufferAppendTest 100 avgt 15 1698.781 ± 80.934 ns/op
StringAppendJmhTest.stringBufferAppendTest 1000 avgt 15 14059.694 ± 820.273 ns/op
StringAppendJmhTest.stringBuilderAppendTest 2 avgt 15 27.621 ± 1.745 ns/op
StringAppendJmhTest.stringBuilderAppendTest 10 avgt 15 154.621 ± 3.360 ns/op
StringAppendJmhTest.stringBuilderAppendTest 100 avgt 15 1488.514 ± 31.618 ns/op
StringAppendJmhTest.stringBuilderAppendTest 1000 avgt 15 12032.867 ± 69.878 ns/op
示例測驗結果中的Score指標,表示ns/op即平均每次呼叫需要多少微秒,時間越低說明效率越高~
1.2.2 圖形展示
程式運行完成后,會在控制臺輸出結果資訊,還會將結果資訊格式化成json格式保存到了桌面的StringAppendJmhTest.json檔案中,將json檔案通過如下可視化工具生成圖形:
JMH Visualizer:https://jmh.morethan.io/JMH Visual Chart:http://deepoove.com/jmh-visual-chart/
測驗結果可視化如下:

1.3 結果分析
字串拼接性能:StringBuilder > StringBuffer > String
通過
JMH的測驗結果,可以發現在少量拼接字串10個左右,效率區別不大,但是當字串拼接的資料量比較大時,100左右,String比另外兩者效率開始相差好幾倍,當達到1000時,此時String的字串拼接效率真的非常差非常差了,比另外兩者效率低了即幾十上百倍,這種情況應當避免使用String來拼接字串~
二、區別說明
2.1 String
2.1.1 String特性
- ? 實作了序列化
Serializable,Comparable以及CharSequence字符序列介面- ?
String是Java字串物件,底層是基于char字符陣列,使用了final修飾類,表示最終類,不能被繼承和修改,執行緒安全~- ? 每一次對
String宣告的物件的內容進行修改,得到的都是另外一個新的字串常量物件,如果字串常量池中已經存在該字串常量物件,則不會再創建~- ?
字串常量在JDK1.7之前,存在于方法區運行時常量池中的字串常量池,JDK1.7時,字串常量池被移到堆區中,運行時常量池還保留在方法區中- ? JDK1.8時,取消了方法區(永久代),方法區被元空間替代,
字串常量拼接還被自動優化成了StringBuiler,例如:
String s1 = “Justin”;
String s2 = “Jack”;
String s3 = s1 + s2;
//先javac編譯java源檔案得到Class,再經過javap -c ClassName反編譯查看匯編指令發現,發現s1+s2等價于
String s4 = new StringBuffer().append(s1).append(s2).toString();- ?
String重寫了Object類中的equals、hashCode方法,重寫后equals方法比較了字串的每一個字符,而重寫hashCode方法則是由字串的每一個字符計算出字串的hashCode值~
2.1.2 String常用API
| 常用方法 | 方法說明 |
|---|---|
int length() | 求字串長度 |
boolean isEmpty() | 判斷字串是否為空字串,注意str.isEmpty()呼叫時,要避免str為null |
String valueOf(Object obj) | 轉換Object型別為字串型別 |
String trim() | 去除字串兩端的空白 |
int indexOf(int ch) | 回傳指定字符在字串中第一次出現的索引,這里的ch指的是char字符對應的ASCII碼值 |
String replace(char oldChar, char newChar) | 替換字串中的字符oldChar為newChar |
String[] split(String regex) | 根據regex分割字串,回傳一個分割后的字串陣列 |
byte[] getBytes() | 獲取字串的 byte型別陣列 |
char charAt(int index) | 獲取指定索引處的字符 |
String toLowerCase() | 將字串中的所有大寫字母轉成小寫字母后回傳新的字串,注意原來的字串沒變 |
String toUpperCase() | 將字串中的所有小寫字母轉成大寫字母后回傳新的字串,注意原來的字串沒變 |
String substring(int beginIndex, int endIndex) | 截取字串,第一位從0開始,包含左邊beginIndex,不包含右邊endIndex |
boolean equals(Object anObject) | 比較字串內容是否相等 |
以上是比較常用的方法,更多可以查看java.lang.String的原始碼~
2.1.3 String常見面試題(附參考答案)
(1)String重寫equals、hashCode方法有什么用??
- 不重寫默認是
Object中的兩個方法,equals默認進行雙等號判斷,比較的是兩個物件的堆區記憶體地址是否相等,而hashCode則是一個native本地方法,內部會自行計算出一個唯一隨機整數值回傳String都重寫了equals、hashCode方法,equals重寫后比較的是字串中的每一個字符,hashCode重寫后則是通過數字31與字串中的每一個字符的ASCII碼值計算得到hashCode值- 簡單的說
String重寫equals、hashCode方法的主要目的是為了比較兩個物件的內容是否相同,而不是比較物件的記憶體地址,因為兩個內容一樣的字串,可能記憶體地址是不相同的,不是我們想要的結果,
(2)重寫String中的
hashCode方法時,為什么要用31這個數字與字串中的每一個字符的ASCII碼值進行計算?
- 因為
31是數學家們計算得到的一個優選質數(如果一個數只能夠被1和本身整除,不能夠被其他數字整除,這個數就是質數,最小質數是2,其他3,5,7,13,17…31…37…)
這個優選質數能夠降低哈希演算法的沖突率,而且31能夠被JVM優化為1右移5位后再減去1即31 * i = (i << 5) - i
(2)new String(“Justin”)創建了幾個物件?
- 一個或者兩個,使用
new實體化,首先肯定會在堆區創建一個新物件,至于new String中指定的字串常量,如果該字串常量在字串常量池中不存在,則會再次創建字串常量池中的物件,一共兩個物件~- 需要注意的是
字串常量池是從JDK1.7開始,就從JVM的方法區遷移到了堆區中了,不是JDK1.8才遷移,JDK1.8是永久代被取消,同時由元空間取代了方法區~
(3)定義String s1=null,String s2="",String s3 = new String(),String s4=new String("")有什么區別?
- 主要區別在于null沒有分配記憶體,其他三種都分配了記憶體空間
空字串也屬于字串常量,定義的參考會直接指向字串常量池中的字串,如果字串常量池不存在空字串,則該程序會在字串常量池中創建空字串的物件,new String()由于使用了new實體化,必然會在堆區創建一個新物件,而new String()底層默認將空字串作為字串物件的值,因此該程序可能創建了1個物件或2個物件- 同樣
new String("")跟new String()一樣也是可能創建了1個物件或2個物件~
(3)String、StringBuilder及StringBuffer最大的區別是什么?
- 最大的區別在于String使用
final修飾,表示最終類,不可繼承和修改,執行緒安全- 而StringBuilder和StringBuffer都是可修改物件,StringBuffer使用
synchronized同步修飾方法,執行緒安全,StringBuilder非執行緒安全~String在JDK1.8時字串常量拼接被自動優化成了StringBuiler- 關于字串拼接效率,我個人通過Open JDK基準性能測驗工具
JMH對三者的new實體化物件,進行字串的拼接測驗,發現效率始終是:
StringBuilder > StringBuffer > String
而且在少量拼接字串10個左右時,三者的拼接效率區別并不大,但是當字串拼接的資料量比較大時,100左右,String比另外兩者效率開始相差好幾倍,當達到1000時,此時String的字串拼接效率真的非常差非常差了,比另外兩者效率低了即幾十上百倍,這種情況應當避免使用String來拼接字串~
2.2 StringBuilder
2.2.1 StringBuilder特性
- ? 底層繼承了
AbstractStringBuilder,實作了Serializable、CharSequence介面- ? 底層基于char字符陣列,可以修改操作物件,非執行緒安全
- ? 實體化
new StringBuffer()時默認位元組陣列初始化容量大小為16,當容量大于當前位元組陣列容量時會自動進行1倍擴容再加2,每次擴容都會開辟新空間,并且進行新老字符陣列的復制- ? 原始碼底層通過呼叫
System的一個native本地方法arraycopy實作新老字符陣列的復制,該native方法底層會直接操作記憶體,比一般的for回圈遍歷復制陣列的效率要快很多~- ? 如果要操作拼接字串,并且拼接的字串很長,又沒有給
StringBuilder指定合適的初始化容量大小,可能會導致底層的字符陣列進行多次擴容,多次申請記憶體空間來完成新老字符陣列的復制,性能開銷比較大~
StringBuilder擴容機制的關鍵原始碼:
//擴容條件:當容量大于當前位元組陣列容量時
if (minimumCapacity - value.length > 0) expandCapacity(minimumCapacity);
...
//擴容多少:會自動進行1倍擴容再加2
int newCapacity = value.length * 2 + 2;
...
//新老字符陣列的復制
value = Arrays.copyOf(value, newCapacity);
...
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
//底層操作記憶體進行復制原字符陣列的元素到新位元組陣列
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
return copy;
}
...
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
2.2.2 StringBuilder常用API
| 常用方法 | 說明 |
|---|---|
StringBuilder append(String str) | 拼接字串 |
String toString() | 回傳字串內容 |
char charAt(int index) | 獲取指定索引的字符 |
StringBuilder insert(int offset, String str) | 在指定位置offset之前插入字串 |
void setCharAt(int index, char ch) | 將指定位置index的字符替換為ch |
StringBuilder insert(int offset, String str) | 在指定位置offset之前插入字串 |
StringBuilder delete(int start, int end | 洗掉起始位置start(含)到結尾位置end(不含)之間的字串 |
其他方法請查看java.lang.StringBuilder原始碼詳情~
2.2.3 StringBuilder常見面試題(附參考答案)
(1)講一下StringBuilder的擴容機制?
- 主要結合StringBuffer特性來回答即可~
- ? 實體化
new StringBuffer()時默認位元組陣列初始化容量大小為16,當容量大于當前位元組陣列容量時會自動進行1倍擴容再加2,每次擴容都會開辟新空間,并且進行新老字符陣列的復制- ? 原始碼底層通過呼叫
System的一個native本地方法arraycopy實作新老字符陣列的復制,該native方法底層會直接操作記憶體,比一般的for回圈遍歷復制陣列的效率要快很多~- ? 如果操作的字串很長,又沒有給
StringBuilder指定合適的初始化容量大小,可能會導致底層的字符陣列進行多次擴容,多次申請記憶體空間來完成新老字符陣列的復制,性能開銷比較大~
(2)String、StringBuilder及StringBuffer最大的區別是什么?
- 同String常見面試問題解答即可~
2.3 StringBuffer
2.3.1 StringBuffer特性
- ? StringBuffer底層實作與StringBuffer最大的區別在于方法使用了
synchronized(自創諧音:星可nice的,哈哈哈)同步修飾,因此是執行緒安全的,StringBuilder非執行緒安全~
2.3.2 StringBuffer常用API
跟StringBuilder常用API一樣,只不過加了synchronized修飾,執行緒安全~
2.3.3 StringBuffer常見面試題(附參考答案)
(1)StringBuffer為什么是執行緒安全的?
- StringBuffer底層使用synchronized同步修飾方法,因此是執行緒安全的~
(2)為什么StringBuffer使用synchronized修飾方法就能保證執行緒安全?
- synchronized是一個同步鎖,在Java中每個類物件都可以作為鎖,synchronized同步鎖使用的關鍵在于對誰加鎖~
- synchronized修飾普通方法
synchronized methodA(){//操作},是對當前物件加鎖~- synchronized修飾靜態方法
static synchronized void methodB(){//操作},是對當前類的class物件(所有此類的物件)加鎖~- synchronized修飾代碼塊
methodC(obj){synchronized(obj) {//操作}},是對括號中的物件加鎖 ~- 因此,使用synchronized修飾方法時,會對方法中的相關物件進行加鎖,如果某個執行緒搶先呼叫了該方法,那么將獨占相關物件的鎖,其他執行緒如果此時呼叫到該方法的相關物件時,會被阻塞~
(3)String、StringBuilder及StringBuffer最大的區別是什么?
- 最大的區別在于String使用
final修飾,表示最終類,不可繼承和修改,執行緒安全- 而StringBuilder和StringBuffer都是可修改物件,StringBuffer使用
synchronized同步修飾方法,執行緒安全,StringBuilder非執行緒安全~String在JDK1.8時字串常量拼接被自動優化成了StringBuiler- 關于字串拼接效率,我個人通過Open JDK基準性能測驗工具
JMH對三者的new實體化物件,進行字串的拼接測驗,發現效率始終是:
StringBuilder > StringBuffer > String
而且在少量拼接字串10個左右時,三者的拼接效率區別并不大,但是當字串拼接的資料量比較大時,100左右,String比另外兩者效率開始相差好幾倍,當達到1000時,此時String的字串拼接效率真的非常差非常差了,比另外兩者效率低了即幾十上百倍,這種情況應當避免使用String來拼接字串~
好了,本文的String、StringBuilder及StringBuffer區別分析就到這里結束啦,如有不妥和不足的地方,歡迎評論區指出糾正,非常感謝!!
原創不易,覺得有用的小伙伴來個一鍵三連(點贊+收藏+評論 )+關注支持一下,非常感謝~


轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/297282.html
標籤:java
上一篇:Java基礎語法筆記
