作者:小傅哥
部落格:https://bugstack.cn
沉澱、分享、成長,讓自己和他人都能有所收穫!?
一、前言
聊的是八股的文,乾的是搬磚的活!
面我的題開發都用不到,你為什麼要問?可能這是大部分程式設計師求職時的經歷,甚至也是大家討厭和煩躁的點。明明給的是擰螺絲的錢、明明做的是寫CRUD的事、明明擔的是成工具的人!
明明... 有很多,可明明公司不會招5年開發做3年經驗的事、明明公司也更喜歡具有附加價值的研發。有些小公司不好說,但在一些網際網路大廠中,我們都希望招聘到具有培養價值的,也更喜歡能快速打怪升級的,也更願意讓這樣的人承擔更大的職責。
但,你酸了! 別人看原始碼你打遊戲、別人學演算法你刷某音、別人寫部落格你浪98。所以,沒有把時間用到個人成長上,就一直會被別人榨取。
二、面試題
謝飛機
,總感覺自己有技術瓶頸、有知識盲區,但是又不知道在哪。所以約面試官聊天,雖然也面不過去!
面試官:飛機,你又抱著大臉,來白嫖我了啦?
謝飛機:嘿嘿,我需要知識,我渴。
面試官:好,那今天聊聊最常用的 String
吧,你怎麼初始化一個字串型別。
謝飛機:String str = "abc";
面試官:還有嗎?
謝飛機:還有?啊,這樣 String str = new String("abc");
?
面試官:還有嗎?
謝飛機:啊!?還有!不知道了!
面試官:你不懂 String
,你沒看過原始碼。還可以這樣;new String(new char[]{'c', 'd'});
回家再學學吧,下次記得給我買百事,我不喝可口。
三、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) + "毫秒");
綜上,分別使用了 String
、StringBuilder
、StringBuffer
,做字串連結操作(100個、1000個、1萬個、10萬個、100萬個),記錄每種方式的耗時。最終彙總圖表如下;
從上圖可以得出以下結論;
String
字串連結是耗時的,尤其資料量大的時候,簡直沒法使用了。這是做實驗,基本也不會有人這麼幹!StringBuilder
、StringBuffer
,因為沒有發生多執行緒競爭也就沒有?鎖升級,所以兩個類耗時幾乎相同,當然在單執行緒下更推薦使用StringBuilder
。
2. StringBuilder 比 String 快, 為什麼?
String str = "";
for (int i = 0; i < 10000; i++) {
str += i;
}
這段程式碼就是三種字串拼接方式,最慢的一種。不是說這種+
加的符號,會被優化成 StringBuilder
嗎,那怎麼還慢?
確實會被JVM編譯期優化,但優化成什麼樣子了呢,先看下位元組碼指令;javap -c ApiTest.class
一看指令碼,這不是在迴圈裡(if_icmpgt)給我 new
了 StringBuilder
了嗎,怎麼還這麼慢呢?再仔細看,其實你會發現,這new是在迴圈裡嗎呀,我們把這段程式碼寫出來再看看;
String str = "";
for (int i = 0; i < 10000; i++) {
str = new StringBuilder().append(str).append(i).toString();
}
現在再看這段程式碼就很清晰了,所有的字串連結操作,都需要例項化一次StringBuilder
,所以非常耗時。並且你可以驗證,這樣寫程式碼耗時與字串直接連結是一樣的。 所以把StringBuilder
提到上一層 for
迴圈外更快。
四、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
的類和用於存放字串的方法都用了 final
修飾,也就是建立了以後,這些都是不可變的。
舉個例子
String str_01 = "abc";
String str_02 = "abc" + "def";
String str_03 = str_01 + "def";
不考慮其他情況,對於程式初始化。以上這些程式碼 str_01
、str_02
、str_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™ 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++語言編寫的功能。
\openjdk8\jdk\src\share\native\java\lang\String.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 問題圖解
看圖說話,如下;
- 先說
==
,基礎型別比對的是值,引用型別比對的是地址。另外,equal 比對的是雜湊值。 - 兩個new出來的物件,地址肯定不同,所以是false。
- intern(),直接把值推進了常量池,所以兩個物件都做了
intern()
操作後,比對是常量池裡的值。 str_3 = "ab"
,賦值,JVM編譯器做了優化,不會重新建立物件,直接引用常量池裡的值。所以str_1.intern() == str_3
,比對結果是true。
理解了這個結構,根本不需要死記硬背應對面試,讓懂了就是真的懂,大腦也會跟著愉悅。
五、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
的時候已經介紹過。
六、StringBuffer 原始碼分析
StringBuffer
與 StringBuilder
,API的使用和底層實現上基本一致,維度不同的是 StringBuffer
加了 synchronized
?鎖,所以它是執行緒安全的。原始碼如下;
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
那麼,synchronized
不是重量級鎖嗎,JVM對它有什麼優化呢?
其實為了減少獲得鎖與釋放鎖帶來的效能損耗,從而引入了偏向鎖、輕量級鎖、重量級鎖來進行優化,它的進行一個鎖升級,如下圖(此圖引自網際網路使用者:韭韭韭韭菜,畫的非常優秀);
- 從無鎖狀態開始,當執行緒進入
synchronized
同步程式碼塊,會檢查物件頭和棧幀內是否有當前線下ID編號,無則使用CAS
替換。 - 解鎖時,會使用
CAS
將Displaced Mark Word
替換回到物件頭,如果成功,則表示競爭沒有發生,反之則表示當前鎖存在競爭鎖就會升級成重量級鎖。 - 另外,大多數情況下鎖?是不發生競爭的,基本由一個執行緒持有。所以,為了避免獲得鎖與釋放鎖帶來的效能損耗,所以引入鎖升級,升級後不能降級。
七、常用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() | 獲取雜湊值 |
八、總結
業精於勤,荒於嬉
,你學到的知識不一定只是為了面試準備,還更應該是擴充自己的技術深度和廣度。這個過程可能很痛苦,但總得需要某一個燒腦的過程,才讓其他更多的知識學起來更加容易。- 本文介紹了
String、StringBuilder、StringBuffer
,的資料結構和原始碼分析,更加透徹的理解後,也能更加準確的使用,不會被因為不懂而犯錯誤。 - 想把程式碼寫好,至少要有這四面內容,包括;資料結構、演算法、原始碼、設計模式,這四方面在加上業務經驗與個人視野,才能真的把一個需求、一個大專案寫的具備良好的擴充套件性和易維護性。