做者:小傅哥
博客:https://bugstack.cnhtml
沉澱、分享、成長,讓本身和他人都能有所收穫!😄
聊的是八股的文,乾的是搬磚的活!
java
面個人題開發都用不到,你爲何要問?可能這是大部分程序員求職時的經歷,甚至也是你們討厭和煩躁的點。明明給的是擰螺絲的錢、明明作的是寫CRUD的事、明明擔的是成工具的人!c++
明明... 有不少,可明明公司不會招5年開發作3年經驗的事、明明公司也更喜歡具備附加價值的研發。有些小公司很差說,但在一些互聯網大廠中,咱們都但願招聘到具備培養價值的,也更喜歡能快速打怪升級的,也更願意讓這樣的人承擔更大的職責。git
但,你酸了! 別人看源碼你打遊戲、別人學算法你刷某音、別人寫博客你浪98。因此,沒有把時間用到我的成長上,就一直會被別人榨取。
程序員
謝飛機
,總感受本身有技術瓶頸、有知識盲區,可是又不知道在哪。因此約面試官聊天,雖然也面不過去!github
面試官:飛機,你又抱着大臉,來白嫖我了啦?面試
謝飛機:嘿嘿,我須要知識,我渴。算法
面試官:好,那今天聊聊最經常使用的 String
吧,你怎麼初始化一個字符串類型。express
謝飛機:String str = "abc";
設計模式
面試官:還有嗎?
謝飛機:還有?啊,這樣 String str = new String("abc");
😄
面試官:還有嗎?
謝飛機:啊!?還有!不知道了!
面試官:你不懂 String
,你沒看過源碼。還能夠這樣;new String(new char[]{'c', 'd'});
回家再學學吧,下次記得給我買百事,我不喝可口。
老子代碼一把梭,總有人絮叨這麼搞很差,那 StringBuilder
到底那快了!
long startTime = System.currentTimeMillis(); String str = ""; for (int i = 0; i < 1000000; i++) { str += i; } System.out.println("String 耗時:" + (System.currentTimeMillis() - startTime) + "毫秒");
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) + "毫秒");
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
。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
循環外更快。
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; ... }
在與 謝飛機
的面試題中,咱們聊到了 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]; }
字符串建立後是不可變的,你看到的+加號
鏈接操做,都是建立了新的對象把數據存放過去,經過源碼就能夠看到;
從源碼中能夠看到,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:()
操做,共建立了三個對象。因此,咱們看到,字符串的建立是不能被修改的,相連操做會建立出新對象。
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
看了答案有點感受了嗎,其實可能你瞭解方法 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++語言編寫的功能。
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); }
1009
個大小,jdk1.6不可調、jdk1.7能夠設置-XX:StringTableSize
,按需調整。看圖說話,以下;
==
,基礎類型比對的是值,引用類型比對的是地址。另外,equal 比對的是哈希值。intern()
操做後,比對是常量池裏的值。str_3 = "ab"
,賦值,JVM編譯器作了優化,不會從新建立對象,直接引用常量池裏的值。因此str_1.intern() == str_3
,比對結果是true。理解了這個結構,根本不須要死記硬背應對面試,讓懂了就是真的懂,大腦也會跟着愉悅。
new StringBuilder(); new StringBuilder(16); new StringBuilder("abc");
這幾種方式均可以初始化,你能夠傳一個初始化容量,也能夠初始化一個默認的字符串。它的源碼以下;
public StringBuilder() { super(16); } AbstractStringBuilder(int capacity) { value = new char[capacity]; }
定睛一看,這就是在初始化數組呀!那是不操做起來跟使用 ArrayList
似的呀!
stringBuilder.append("a"); stringBuilder.append("b"); stringBuilder.append("c");
添加元素的操做很簡單,使用 append
便可,那麼它是怎麼往數組中存放的呢,須要擴容嗎?
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
數量。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,另外把原有元素拷貝到新新數組中。
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
拷貝操做進行的,這是一個本地方法。
既然 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
與 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
替換回到對象頭,若是成功,則表示競爭沒有發生,反之則表示當前鎖存在競爭鎖就會升級成重量級鎖。序號 | 方法 | 描述 |
---|---|---|
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
,的數據結構和源碼分析,更加透徹的理解後,也能更加準確的使用,不會被由於不懂而犯錯誤。