StringBuilder 比 String 快?空嘴白牙的,證據呢!

做者:小傅哥
博客:https://bugstack.cnhtml

沉澱、分享、成長,讓本身和他人都能有所收穫!😄

1、前言

聊的是八股的文,乾的是搬磚的活!java

面個人題開發都用不到,你爲何要問?可能這是大部分程序員求職時的經歷,甚至也是你們討厭和煩躁的點。明明給的是擰螺絲的錢、明明作的是寫CRUD的事、明明擔的是成工具的人!c++

明明... 有不少,可明明公司不會招5年開發作3年經驗的事、明明公司也更喜歡具備附加價值的研發。有些小公司很差說,但在一些互聯網大廠中,咱們都但願招聘到具備培養價值的,也更喜歡能快速打怪升級的,也更願意讓這樣的人承擔更大的職責。git

但,你酸了! 別人看源碼你打遊戲、別人學算法你刷某音、別人寫博客你浪98。因此,沒有把時間用到我的成長上,就一直會被別人榨取。程序員

2、面試題

謝飛機,總感受本身有技術瓶頸、有知識盲區,可是又不知道在哪。因此約面試官聊天,雖然也面不過去!github

面試官:飛機,你又抱着大臉,來白嫖我了啦?面試

謝飛機:嘿嘿,我須要知識,我渴。算法

面試官:好,那今天聊聊最經常使用的 String 吧,你怎麼初始化一個字符串類型。express

謝飛機String str = "abc"; 設計模式

面試官:還有嗎?

謝飛機:還有?啊,這樣 String str = new String("abc"); 😄

面試官:還有嗎?

謝飛機:啊!?還有!不知道了!

面試官:你不懂 String,你沒看過源碼。還能夠這樣;new String(new char[]{'c', 'd'}); 回家再學學吧,下次記得給我買百事,我不喝可口

3、StringBuilder 比 String 快嗎?

1. StringBuilder 比 String 快,證據呢?

老子代碼一把梭,總有人絮叨這麼搞很差,那 StringBuilder 到底那快了!

1.1 String

long startTime = System.currentTimeMillis();
String str = "";
for (int i = 0; i < 1000000; i++) {
    str += i;
}
System.out.println("String 耗時:" + (System.currentTimeMillis() - startTime) + "毫秒");

1.2 StringBuilder

long startTime = System.currentTimeMillis();
StringBuilder str = new StringBuilder();
for (int i = 0; i < 1000000; i++) {
    str.append(i);
}
System.out.println("StringBuilder 耗時" + (System.currentTimeMillis() - startTime) + "毫秒");

1.3 StringBuffer

long startTime = System.currentTimeMillis();
StringBuffer str = new StringBuffer();
for (int i = 0; i < 1000000; i++) {
    str.append(i);
}
System.out.println("StringBuffer 耗時" + (System.currentTimeMillis() - startTime) + "毫秒");

綜上,分別使用了 StringStringBuilderStringBuffer,作字符串連接操做(100個、1000個、1萬個、10萬個、100萬個),記錄每種方式的耗時。最終彙總圖表以下;

小傅哥 & 耗時對比

從上圖能夠得出如下結論;

  1. String 字符串連接是耗時的,尤爲數據量大的時候,簡直無法使用了。這是作實驗,基本也不會有人這麼幹!
  2. StringBuilderStringBuffer,由於沒有發生多線程競爭也就沒有🔒鎖升級,因此兩個類耗時幾乎相同,固然在單線程下更推薦使用 StringBuilder

2. StringBuilder 比 String 快, 爲何?

String str = "";
for (int i = 0; i < 10000; i++) {
    str += i;
}

這段代碼就是三種字符串拼接方式,最慢的一種。不是說這種+加的符號,會被優化成 StringBuilder 嗎,那怎麼還慢?

確實會被JVM編譯期優化,但優化成什麼樣子了呢,先看下字節碼指令;javap -c ApiTest.class

小傅哥 & 反編譯

一看指令碼,這不是在循環裏(if_icmpgt)給我 newStringBuilder 了嗎,怎麼還這麼慢呢?再仔細看,其實你會發現,這new是在循環裏嗎呀,咱們把這段代碼寫出來再看看;

String str = "";
for (int i = 0; i < 10000; i++) {
    str = new StringBuilder().append(str).append(i).toString();
}

如今再看這段代碼就很清晰了,全部的字符串連接操做,都須要實例化一次StringBuilder,因此很是耗時。而且你能夠驗證,這樣寫代碼耗時與字符串直接連接是同樣的。 因此把StringBuilder 提到上一層 for 循環外更快。

4、String 源碼分析

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
     
    ...
}

1. 初始化

在與 謝飛機 的面試題中,咱們聊到了 String 初始化的問題,按照通常咱們應用的頻次上,能想到的只有直接賦值,String str = "abc"; ,但由於 String 的底層數據結構是數組char value[],因此它的初始化方式也會有不少跟數組相關的,以下;

String str_01 = "abc";
System.out.println("默認方式:" + str_01);

String str_02 = new String(new char[]{'a', 'b', 'c'});
System.out.println("char方式:" + str_02);

String str_03 = new String(new int[]{0x61, 0x62, 0x63}, 0, 3);
System.out.println("int方式:" + str_03);

String str_04 = new String(new byte[]{0x61, 0x62, 0x63});
System.out.println("byte方式:" + str_04);

以上這些方式均可以初始化,而且最終的結果是一致的,abc。若是說初始化的方式沒用讓你感覺到它是數據結構,那麼str_01.charAt(0);呢,只要你往源碼裏一點,就會發現它是 O(1) 的時間複雜度從數組中獲取元素,因此效率也是很是高,源碼以下;

public char charAt(int index) {
    if ((index < 0) || (index >= value.length)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return value[index];
}

2. 不可變(final)

字符串建立後是不可變的,你看到的+加號鏈接操做,都是建立了新的對象把數據存放過去,經過源碼就能夠看到;

小傅哥 & String 不可變

從源碼中能夠看到,String 的類和用於存放字符串的方法都用了 final 修飾,也就是建立了之後,這些都是不可變的。

舉個例子

String str_01 = "abc";
String str_02 = "abc" + "def";
String str_03 = str_01 + "def";

不考慮其餘狀況,對於程序初始化。以上這些代碼 str_01str_02str_03,都會初始化幾個對象呢?其實這個初始化幾個對象從側面就是反應對象是否可變性。

接下來咱們把上面代碼反編譯,經過指令碼看到底建立了幾個對象。

反編譯下

public void test_00();
    Code:
       0: ldc           #2                  // String abc
       2: astore_1
       3: ldc           #3                  // String abcdef
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_1
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: ldc           #7                  // String def
      19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      25: astore_3
      26: return
  • str_01 = "abc",指令碼:0: ldc,建立了一個對象。
  • str_02 = "abc" + "def",指令碼:3: ldc // String abcdef,得益於JVM編譯期的優化,兩個字符串會進行相連,建立一個對象存儲。
  • str_03 = str_01 + "def",指令碼:invokevirtual,這個就不同了,它須要把兩個字符串相連,會建立StringBuilder對象,直至最後toString:()操做,共建立了三個對象。

因此,咱們看到,字符串的建立是不能被修改的,相連操做會建立出新對象。

3. intern()

3.1 經典題目

String str_1 = new String("ab");
String str_2 = new String("ab");
String str_3 = "ab";

System.out.println(str_1 == str_2);
System.out.println(str_1 == str_2.intern());
System.out.println(str_1.intern() == str_2.intern());
System.out.println(str_1 == str_3);
System.out.println(str_1.intern() == str_3);

這是一道經典的 String 字符串面試題,乍一看可能還會有點暈。答案以下;

false
false
true
false
true

3.2 源碼分析

看了答案有點感受了嗎,其實可能你瞭解方法 intern(),這裏先看下它的源碼;

/**
 * Returns a canonical representation for the string object.
 * <p>
 * A pool of strings, initially empty, is maintained privately by the
 * class {@code String}.
 * <p>
 * When the intern method is invoked, if the pool already contains a
 * string equal to this {@code String} object as determined by
 * the {@link #equals(Object)} method, then the string from the pool is
 * returned. Otherwise, this {@code String} object is added to the
 * pool and a reference to this {@code String} object is returned.
 * <p>
 * It follows that for any two strings {@code s} and {@code t},
 * {@code s.intern() == t.intern()} is {@code true}
 * if and only if {@code s.equals(t)} is {@code true}.
 * <p>
 * All literal strings and string-valued constant expressions are
 * interned. String literals are defined in section 3.10.5 of the
 * <cite>The Java&trade; Language Specification</cite>.
 *
 * @return  a string that has the same contents as this string, but is
 *          guaranteed to be from a pool of unique strings.
 */
public native String intern();

這段代碼和註釋什麼意思呢?

native,說明 intern() 是一個本地方法,底層經過JNI調用C++語言編寫的功能。

openjdk8jdksrcsharenativejavalangString.c

Java_java_lang_String_intern(JNIEnv *env, jobject this)  
{  
    return JVM_InternString(env, this);  
}  

oop result = StringTable::intern(string, CHECK_NULL);

oop StringTable::intern(Handle string_or_null, jchar* name,  
                        int len, TRAPS) {  
  unsigned int hashValue = java_lang_String::hash_string(name, len);  
  int index = the_table()->hash_to_index(hashValue);  
  oop string = the_table()->lookup(index, name, len, hashValue);  
  if (string != NULL) return string;   
  return the_table()->basic_add(index, string_or_null, name, len,  
                                hashValue, CHECK_NULL);  
}
  • 代碼塊有點長這裏只截取了部份內容,源碼能夠學習開源jdk代碼,鏈接: https://codeload.github.com/abhijangda/OpenJDK8/zip/master
  • C++這段代碼有點像HashMap的哈希桶+鏈表的數據結構,用來存放字符串,因此若是哈希值衝突嚴重,就會致使鏈表過長。這在咱們講解hashMap中已經介紹,能夠回看 HashMap源碼
  • StringTable 是一個固定長度的數組 1009 個大小,jdk1.6不可調、jdk1.7能夠設置-XX:StringTableSize,按需調整。

3.3 問題圖解

小傅哥 & 圖解true/false

看圖說話,以下;

  1. 先說 ==,基礎類型比對的是值,引用類型比對的是地址。另外,equal 比對的是哈希值。
  2. 兩個new出來的對象,地址確定不一樣,因此是false。
  3. intern(),直接把值推動了常量池,因此兩個對象都作了 intern() 操做後,比對是常量池裏的值。
  4. str_3 = "ab",賦值,JVM編譯器作了優化,不會從新建立對象,直接引用常量池裏的值。因此str_1.intern() == str_3,比對結果是true。

理解了這個結構,根本不須要死記硬背應對面試,讓懂了就是真的懂,大腦也會跟着愉悅。

5、StringBuilder 源碼分析

1. 初始化

new StringBuilder();
new StringBuilder(16);
new StringBuilder("abc");

這幾種方式均可以初始化,你能夠傳一個初始化容量,也能夠初始化一個默認的字符串。它的源碼以下;

public StringBuilder() {
    super(16);
}

AbstractStringBuilder(int capacity) {
    value = new char[capacity];
}

定睛一看,這就是在初始化數組呀!那是不操做起來跟使用 ArrayList 似的呀!

2. 添加元素

stringBuilder.append("a");
stringBuilder.append("b");
stringBuilder.append("c");

添加元素的操做很簡單,使用 append 便可,那麼它是怎麼往數組中存放的呢,須要擴容嗎?

2.1 入口方法

public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}
  • 這個是 public final class StringBuilder extends AbstractStringBuilder,的父類與 StringBuffer 共用這個方法。
  • 這裏包括了容量檢測、元素拷貝、記錄 count 數量。

2.2 擴容操做

ensureCapacityInternal(count + len);

/**
 * This method has the same contract as ensureCapacity, but is
 * never synchronized.
 */
private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    if (minimumCapacity - value.length > 0)
        expandCapacity(minimumCapacity);
}

/**
 * This implements the expansion semantics of ensureCapacity with no
 * size check or synchronization.
 */
void expandCapacity(int minimumCapacity) {
    int newCapacity = value.length * 2 + 2;
    if (newCapacity - minimumCapacity < 0)
        newCapacity = minimumCapacity;
    if (newCapacity < 0) {
        if (minimumCapacity < 0) // overflow
            throw new OutOfMemoryError();
        newCapacity = Integer.MAX_VALUE;
    }
    value = Arrays.copyOf(value, newCapacity);
}

如上,StringBuilder,就跟操做數組的原理同樣,都須要檢測容量大小,按需擴容。擴容的容量是 n * 2 + 2,另外把原有元素拷貝到新新數組中。

2.3 填充元素

str.getChars(0, len, value, count);

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    // ...
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}

添加元素的方式是基於 System.arraycopy 拷貝操做進行的,這是一個本地方法。

2.4 toString()

既然 stringBuilder 是數組,那麼它是怎麼轉換成字符串的呢?

stringBuilder.toString();

@Override
public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

其實須要用到它是 String 字符串的時候,就是使用 String 的構造函數傳遞數組進行轉換的,這個方法在咱們上面講解 String 的時候已經介紹過。

6、StringBuffer 源碼分析

StringBufferStringBuilder,API的使用和底層實現上基本一致,維度不一樣的是 StringBuffer 加了 synchronized 🔒鎖,因此它是線程安全的。源碼以下;

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

那麼,synchronized 不是重量級鎖嗎,JVM對它有什麼優化呢?

其實爲了減小得到鎖與釋放鎖帶來的性能損耗,從而引入了偏向鎖、輕量級鎖、重量級鎖來進行優化,它的進行一個鎖升級,以下圖(此圖引自互聯網用戶:韭韭韭韭菜,畫的很是優秀);

小傅哥 & 此圖引自互聯網,畫的很是漂亮

  1. 從無鎖狀態開始,當線程進入 synchronized 同步代碼塊,會檢查對象頭和棧幀內是否有當前線下ID編號,無則使用 CAS 替換。
  2. 解鎖時,會使用 CASDisplaced Mark Word 替換回到對象頭,若是成功,則表示競爭沒有發生,反之則表示當前鎖存在競爭鎖就會升級成重量級鎖。
  3. 另外,大多數狀況下鎖🔒是不發生競爭的,基本由一個線程持有。因此,爲了不得到鎖與釋放鎖帶來的性能損耗,因此引入鎖升級,升級後不能降級。

7、經常使用API

序號 方法 描述
1 str.concat("cde") 字符串鏈接,替換+號
2 str.length() 獲取長度
3 isEmpty() 判空
4 str.charAt(0) 獲取指定位置元素
5 str.codePointAt(0) 獲取指定位置元素,並返回ascii碼值
6 str.getBytes() 獲取byte[]
7 str.equals("abc") 比較
8 str.equalsIgnoreCase("AbC") 忽略大小寫,比對
9 str.startsWith("a") 開始位置值判斷
10 str.endsWith("c") 結尾位置值判斷
11 str.indexOf("b") 判斷元素位置,開始位置
12 str.lastIndexOf("b") 判斷元素位置,結尾位置
13 str.substring(0, 1) 截取
14 str.split(",") 拆分,能夠支持正則
15 str.replace("a","d")、replaceAll 替換
16 str.toUpperCase() 轉大寫
17 str.toLowerCase() 轉小寫
18 str.toCharArray() 轉數組
19 String.format(str, "") 格式化,%s、%c、%b、%d、%x、%o、%f、%a、%e、%g、%h、%%、%n、%tx
20 str.valueOf("123") 轉字符串
21 trim() 格式化,首尾去空格
22 str.hashCode() 獲取哈希值

8、總結

  • 業精於勤,荒於嬉,你學到的知識不必定只是爲了面試準備,還更應該是拓展本身的技術深度和廣度。這個過程可能很痛苦,但總得須要某一個燒腦的過程,才讓其餘更多的知識學起來更加容易。
  • 本文介紹了 String、StringBuilder、StringBuffer,的數據結構和源碼分析,更加透徹的理解後,也能更加準確的使用,不會被由於不懂而犯錯誤。
  • 想把代碼寫好,至少要有這四面內容,包括;數據結構、算法、源碼、設計模式,這四方面在加上業務經驗與我的視野,才能真的把一個需求、一個大項目寫的具有良好的擴展性和易維護性。

9、系列推薦

相關文章
相關標籤/搜索