作者:小傅哥 博客:https://
沉淀、分享、成長(zhǎng),讓自己和他人都能有所收獲!😄
一、前言
聊的是八股的文,干的是搬磚的活!
面我的題開發(fā)都用不到,你為什么要問(wèn)?可能這是大部分程序員求職時(shí)的經(jīng)歷,甚至也是大家討厭和煩躁的點(diǎn)。明明給的是擰螺絲的錢、明明做的是寫CRUD的事、明明擔(dān)的是成工具的人!
明明… 有很多 ,可明明公司不會(huì)招5年開發(fā)做3年經(jīng)驗(yàn)的事、明明公司也更喜歡具有附加價(jià)值的研發(fā)。有些小公司不好說(shuō),但在一些互聯(lián)網(wǎng)大廠中,我們都希望招聘到具有培養(yǎng)價(jià)值的,也更喜歡能快速打怪升級(jí)的,也更愿意讓這樣的人承擔(dān)更大的職責(zé)。
但,你酸了! 別人看源碼你打游戲、別人學(xué)算法你刷某音、別人寫博客你浪98。所以,沒有把時(shí)間用到個(gè)人成長(zhǎng)上,就一直會(huì)被別人榨取。
二、面試題
謝飛機(jī)
,總感覺自己有技術(shù)瓶頸、有知識(shí)盲區(qū),但是又不知道在哪。所以約面試官聊天,雖然也面不過(guò)去!
面試官 :飛機(jī),你又抱著大臉,來(lái)白嫖我了啦?
謝飛機(jī) :嘿嘿,我需要知識(shí),我渴。
面試官 :好,那今天聊聊最常用的 String
吧,你怎么初始化一個(gè)字符串類型。
謝飛機(jī) :String str = "abc";
面試官 :還有嗎?
謝飛機(jī) :還有?啊,這樣 String str = new String("abc");
😄
面試官 :還有嗎?
謝飛機(jī) :啊!?還有!不知道了!
面試官 :你不懂 String
,你沒看過(guò)源碼。還可以這樣;new String(new char[]{'c', 'd'});
回家再學(xué)學(xué)吧,下次記得給我買百事 ,我不喝可口 。
三、StringBuilder 比 String 快嗎?
1. StringBuilder 比 String 快,證據(jù)呢?
老子代碼一把梭,總有人絮叨這么搞不好,那 StringBuilder
到底那快了!
1.1 String
long startTime = System. currentTimeMillis ( ) ;
String str = "" ;
for ( int i = 0 ; i < 1000000 ; i++ ) {
str += i;
}
System. out. println ( "String 耗時(shí):" + ( 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 耗時(shí)" + ( 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 耗時(shí)" + ( System. currentTimeMillis ( ) - startTime) + "毫秒" ) ;
綜上 ,分別使用了 String
、StringBuilder
、StringBuffer
,做字符串鏈接操作(100個(gè)、1000個(gè)、1萬(wàn)個(gè)、10萬(wàn)個(gè)、100萬(wàn)個(gè) ),記錄每種方式的耗時(shí)。最終匯總圖表如下;
從上圖可以得出以下結(jié)論;
String
字符串鏈接是耗時(shí)的,尤其數(shù)據(jù)量大的時(shí)候,簡(jiǎn)直沒法使用了。這是做實(shí)驗(yàn),基本也不會(huì)有人這么干! StringBuilder
、StringBuffer
,因?yàn)闆]有發(fā)生多線程競(jìng)爭(zhēng)也就沒有🔒鎖升級(jí),所以兩個(gè)類耗時(shí)幾乎相同,當(dāng)然在單線程下更推薦使用 StringBuilder
。
2. StringBuilder 比 String 快, 為什么?
String str = "" ;
for ( int i = 0 ; i < 10000 ; i++ ) {
str += i;
}
這段代碼就是三種字符串拼接方式,最慢的一種。不是說(shuō)這種+
加的符號(hào),會(huì)被優(yōu)化成 StringBuilder
嗎,那怎么還慢?
確實(shí)會(huì)被JVM編譯期優(yōu)化,但優(yōu)化成什么樣子了呢,先看下字節(jié)碼指令;javap -c ApiTest.class
一看指令碼,這不是在循環(huán)里(if_icmpgt )給我 new
了 StringBuilder
了嗎,怎么還這么慢呢?再仔細(xì)看,其實(shí)你會(huì)發(fā)現(xiàn),這new是在循環(huán)里嗎呀,我們把這段代碼寫出來(lái)再看看;
String str = "" ;
for ( int i = 0 ; i < 10000 ; i++ ) {
str = new StringBuilder ( ) . append ( str) . append ( i) . toString ( ) ;
}
現(xiàn)在再看這段代碼就很清晰了,所有的字符串鏈接操作,都需要實(shí)例化一次StringBuilder
,所以非常耗時(shí)。并且你可以驗(yàn)證,這樣寫代碼耗時(shí)與字符串直接鏈接是一樣的。 所以把StringBuilder
提到上一層 for
循環(huán)外更快。
四、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 = - 6849794470754667710 L;
. . .
}
1. 初始化
在與 謝飛機(jī)
的面試題中,我們聊到了 String
初始化的問(wèn)題,按照一般我們應(yīng)用的頻次上,能想到的只有直接賦值,String str = "abc";
,但因?yàn)?String 的底層數(shù)據(jù)結(jié)構(gòu)是數(shù)組char value[]
,所以它的初始化方式也會(huì)有很多跟數(shù)組相關(guān)的,如下;
String str_01 = "abc" ;
System. out. println ( "默認(rèn)方式:" + 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) ;
以上這些方式都可以初始化,并且最終的結(jié)果是一致的,abc
。如果說(shuō)初始化的方式?jīng)]用讓你感受到它是數(shù)據(jù)結(jié)構(gòu),那么str_01.charAt(0);
呢,只要你往源碼里一點(diǎn),就會(huì)發(fā)現(xiàn)它是 O(1)
的時(shí)間復(fù)雜度從數(shù)組中獲取元素,所以效率也是非常高,源碼如下;
public char charAt ( int index) {
if ( ( index < 0 ) || ( index >= value. length) ) {
throw new StringIndexOutOfBoundsException ( index) ;
}
return value[ index] ;
}
2. 不可變(final)
字符串創(chuàng)建后是不可變的,你看到的+加號(hào)
連接操作,都是創(chuàng)建了新的對(duì)象把數(shù)據(jù)存放過(guò)去,通過(guò)源碼就可以看到;
從源碼中可以看到,String
的類和用于存放字符串的方法都用了 final
修飾,也就是創(chuàng)建了以后,這些都是不可變的。
舉個(gè)例子
String str_01 = "abc" ;
String str_02 = "abc" + "def" ;
String str_03 = str_01 + "def" ;
不考慮其他情況,對(duì)于程序初始化。以上這些代碼 str_01
、str_02
、str_03
,都會(huì)初始化幾個(gè)對(duì)象呢?其實(shí)這個(gè)初始化幾個(gè)對(duì)象從側(cè)面就是反應(yīng)對(duì)象是否可變性。
接下來(lái)我們把上面代碼反編譯,通過(guò)指令碼看到底創(chuàng)建了幾個(gè)對(duì)象。
反編譯下
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
,創(chuàng)建了一個(gè)對(duì)象。str_02 = "abc" + "def"
,指令碼:3: ldc // String abcdef
,得益于JVM編譯期的優(yōu)化,兩個(gè)字符串會(huì)進(jìn)行相連,創(chuàng)建一個(gè)對(duì)象存儲(chǔ)。str_03 = str_01 + "def"
,指令碼:invokevirtual
,這個(gè)就不一樣了,它需要把兩個(gè)字符串相連,會(huì)創(chuàng)建StringBuilder
對(duì)象,直至最后toString:()
操作,共創(chuàng)建了三個(gè)對(duì)象。
所以 ,我們看到,字符串的創(chuàng)建是不能被修改的,相連操作會(huì)創(chuàng)建出新對(duì)象。
3. intern()
3.1 經(jīng)典題目
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) ;
這是一道經(jīng)典的 String
字符串面試題,乍一看可能還會(huì)有點(diǎn)暈。答案如下;
false
false
true
false
true
3.2 源碼分析
看了答案有點(diǎn)感覺了嗎,其實(shí)可能你了解方法 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 ,說(shuō)明 intern()
是一個(gè)本地方法,底層通過(guò)JNI調(diào)用C++語(yǔ)言編寫的功能。
\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);
}
代碼塊有點(diǎn)長(zhǎng)這里只截取了部分內(nèi)容,源碼可以學(xué)習(xí)開源jdk代碼,連接: https://codeload.github.com/abhijangda/OpenJDK8/zip/master C++這段代碼有點(diǎn)像HashMap的哈希桶+鏈表的數(shù)據(jù)結(jié)構(gòu),用來(lái)存放字符串,所以如果哈希值沖突嚴(yán)重,就會(huì)導(dǎo)致鏈表過(guò)長(zhǎng)。這在我們講解hashMap中已經(jīng)介紹,可以回看 HashMap源碼 StringTable 是一個(gè)固定長(zhǎng)度的數(shù)組 1009
個(gè)大小,jdk1.6不可調(diào)、jdk1.7可以設(shè)置-XX:StringTableSize
,按需調(diào)整。
3.3 問(wèn)題圖解
看圖說(shuō)話,如下;
先說(shuō) ==
,基礎(chǔ)類型比對(duì)的是值,引用類型比對(duì)的是地址。另外,equal 比對(duì)的是哈希值。 兩個(gè)new出來(lái)的對(duì)象,地址肯定不同,所以是false。 intern(),直接把值推進(jìn)了常量池,所以兩個(gè)對(duì)象都做了 intern()
操作后,比對(duì)是常量池里的值。 str_3 = "ab"
,賦值,JVM編譯器做了優(yōu)化,不會(huì)重新創(chuàng)建對(duì)象,直接引用常量池里的值。所以str_1.intern() == str_3
,比對(duì)結(jié)果是true。
理解了這個(gè)結(jié)構(gòu),根本不需要死記硬背應(yīng)對(duì)面試,讓懂了就是真的懂,大腦也會(huì)跟著愉悅。
五、StringBuilder 源碼分析
1. 初始化
new StringBuilder ( ) ;
new StringBuilder ( 16 ) ;
new StringBuilder ( "abc" ) ;
這幾種方式都可以初始化,你可以傳一個(gè)初始化容量,也可以初始化一個(gè)默認(rèn)的字符串。它的源碼如下;
public StringBuilder ( ) {
super ( 16 ) ;
}
AbstractStringBuilder ( int capacity) {
value = new char [ capacity] ;
}
定睛一看,這就是在初始化數(shù)組呀!那是不操作起來(lái)跟使用 ArrayList
似的呀!
2. 添加元素
stringBuilder. append ( "a" ) ;
stringBuilder. append ( "b" ) ;
stringBuilder. append ( "c" ) ;
添加元素的操作很簡(jiǎn)單,使用 append
即可,那么它是怎么往數(shù)組中存放的呢,需要擴(kuò)容嗎?
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 ;
}
這個(gè)是 public final class StringBuilder extends AbstractStringBuilder
,的父類與 StringBuffer
共用這個(gè)方法。 這里包括了容量檢測(cè)、元素拷貝、記錄 count
數(shù)量。
2.2 擴(kuò)容操作
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
,就跟操作數(shù)組的原理一樣,都需要檢測(cè)容量大小,按需擴(kuò)容。擴(kuò)容的容量是 n * 2 + 2,另外把原有元素拷貝到新新數(shù)組中。
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
拷貝操作進(jìn)行的,這是一個(gè)本地方法。
2.4 toString()
既然 stringBuilder
是數(shù)組,那么它是怎么轉(zhuǎn)換成字符串的呢?
stringBuilder.toString();
@Override
public String toString ( ) {
// Create a copy, don't share the array
return new String ( value, 0 , count) ;
}
其實(shí)需要用到它是 String
字符串的時(shí)候,就是使用 String
的構(gòu)造函數(shù)傳遞數(shù)組進(jìn)行轉(zhuǎn)換的,這個(gè)方法在我們上面講解 String
的時(shí)候已經(jīng)介紹過(guò)。
六、StringBuffer 源碼分析
StringBuffer
與 StringBuilder
,API的使用和底層實(shí)現(xiàn)上基本一致,維度不同的是 StringBuffer
加了 synchronized
🔒鎖,所以它是線程安全的。源碼如下;
@Override
public synchronized StringBuffer append ( String str) {
toStringCache = null;
super . append ( str) ;
return this ;
}
那么,synchronized
不是重量級(jí)鎖嗎,JVM對(duì)它有什么優(yōu)化呢?
其實(shí)為了減少獲得鎖與釋放鎖帶來(lái)的性能損耗,從而引入了偏向鎖、輕量級(jí)鎖、重量級(jí)鎖來(lái)進(jìn)行優(yōu)化,它的進(jìn)行一個(gè)鎖升級(jí),如下圖(此圖引自互聯(lián)網(wǎng)用戶:韭韭韭韭菜 ,畫的非常優(yōu)秀);
從無(wú)鎖狀態(tài)開始,當(dāng)線程進(jìn)入 synchronized
同步代碼塊,會(huì)檢查對(duì)象頭和棧幀內(nèi)是否有當(dāng)前線下ID編號(hào),無(wú)則使用 CAS
替換。 解鎖時(shí),會(huì)使用 CAS
將 Displaced Mark Word
替換回到對(duì)象頭,如果成功,則表示競(jìng)爭(zhēng)沒有發(fā)生,反之則表示當(dāng)前鎖存在競(jìng)爭(zhēng)鎖就會(huì)升級(jí)成重量級(jí)鎖。 另外,大多數(shù)情況下鎖🔒是不發(fā)生競(jìng)爭(zhēng)的,基本由一個(gè)線程持有。所以,為了避免獲得鎖與釋放鎖帶來(lái)的性能損耗,所以引入鎖升級(jí),升級(jí)后不能降級(jí)。
七、常用API
序號(hào) 方法 描述 1 str.concat("cde")
字符串連接,替換+號(hào) 2 str.length()
獲取長(zhǎng)度 3 isEmpty()
判空 4 str.charAt(0)
獲取指定位置元素 5 str.codePointAt(0)
獲取指定位置元素,并返回ascii碼值 6 str.getBytes() 獲取byte[] 7 str.equals(“abc”) 比較 8 str.equalsIgnoreCase(“AbC”) 忽略大小寫,比對(duì) 9 str.startsWith(“a”) 開始位置值判斷 10 str.endsWith(“c”) 結(jié)尾位置值判斷 11 str.indexOf(“b”) 判斷元素位置,開始位置 12 str.lastIndexOf(“b”) 判斷元素位置,結(jié)尾位置 13 str.substring(0, 1) 截取 14 str.split(",") 拆分,可以支持正則 15 str.replace(“a”,“d”)、replaceAll 替換 16 str.toUpperCase() 轉(zhuǎn)大寫 17 str.toLowerCase() 轉(zhuǎn)小寫 18 str.toCharArray() 轉(zhuǎn)數(shù)組 19 String.format(str, “”) 格式化,%s、%c、%b、%d、%x、%o、%f、%a、%e、%g、%h、%%、%n、%tx 20 str.valueOf(“123”) 轉(zhuǎn)字符串 21 trim() 格式化,首尾去空格 22 str.hashCode() 獲取哈希值
八、總結(jié)
業(yè)精于勤,荒于嬉
,你學(xué)到的知識(shí)不一定只是為了面試準(zhǔn)備,還更應(yīng)該是拓展自己的技術(shù)深度和廣度。這個(gè)過(guò)程可能很痛苦,但總得需要某一個(gè)燒腦的過(guò)程,才讓其他更多的知識(shí)學(xué)起來(lái)更加容易。本文介紹了 String、StringBuilder、StringBuffer
,的數(shù)據(jù)結(jié)構(gòu)和源碼分析,更加透徹的理解后,也能更加準(zhǔn)確的使用,不會(huì)被因?yàn)椴欢稿e(cuò)誤。 想把代碼寫好,至少要有這四面內(nèi)容,包括;數(shù)據(jù)結(jié)構(gòu)、算法、源碼、設(shè)計(jì)模式,這四方面在加上業(yè)務(wù)經(jīng)驗(yàn)與個(gè)人視野,才能真的把一個(gè)需求、一個(gè)大項(xiàng)目寫的具備良好的擴(kuò)展性和易維護(hù)性。
九、系列推薦
握草,你竟然在代碼里下毒! 一次代碼評(píng)審,差點(diǎn)過(guò)不了試用期! LinkedList插入速度比ArrayList快?你確定嗎? 重學(xué)Java設(shè)計(jì)模式(22個(gè)真實(shí)開發(fā)場(chǎng)景) 面經(jīng)手冊(cè)(上最快的車,拿最貴的offer)