面試——StringBuilder 和 StringBuffer 選錯(cuò)一個(gè))
快手客戶端性能組面試有個(gè)習(xí)慣不考你多炫的架構(gòu)先問字符串拼接。App 里有個(gè)接口要拼上千條日志你用還是 StringBuilder——看似送分答錯(cuò)的人能有一半。說白了String、StringBuilder、StringBuffer 三兄弟初級(jí)崗背定義快手要的是你真的在性能敏感路徑上趟過坑。今天把底層結(jié)構(gòu)、性能差異、編譯器優(yōu)化一次性講清。一、三者底層到底差在哪面試官String、StringBuilder、StringBuffer 區(qū)別候選人標(biāo)準(zhǔn)答法String 不可變后兩個(gè)可變StringBuffer 線程安全StringBuilder 不是。深度解析從存儲(chǔ)結(jié)構(gòu)看就明白了。StringJava 8 是private final char value[]Java 9 是byte[]緊湊字符串。不可變?nèi)魏胃亩挤祷匦聦?duì)象。StringBuilder / StringBuffer內(nèi)部都是一個(gè)可變的 char[] 緩沖區(qū)默認(rèn)容量 16。append()往里填不夠就擴(kuò)容。區(qū)別只在——StringBuffer 的每個(gè) public 方法都加了synchronizedStringBuilder 沒有。// AbstractStringBuilder 的共同底層 char[] value; // 緩沖區(qū) int count; // 已用長(zhǎng)度普通答法 vs 高分答法普通StringBuffer 線程安全所以慢StringBuilder 快。高分我會(huì)點(diǎn)出線程安全的代價(jià)是每次方法調(diào)用都要拿鎖即使單線程也在空轉(zhuǎn)同步所以在明確單線程的場(chǎng)景StringBuffer 是純粹的性能浪費(fèi)。快手這種高并發(fā)客戶端主線程拼字符串用 StringBuilder不要無腦 StringBuffer。有意思的是很多人不知道StringBuffer 的toString()在舊 JDK 也是synchronized的新版本優(yōu)化掉了但方法級(jí)的鎖開銷仍在。二、為什么 拼接會(huì)慢性能真相本篇第二道核心問答也是這道題的靈魂。面試官那s x到底慢在哪深度解析關(guān)鍵看是不是在循環(huán)里。單條語句String s a b c;編譯器會(huì)優(yōu)化見第三組幾乎沒額外開銷。但循環(huán)里的是災(zāi)難var s for (i in 0 until 10000) { s i // 每一輪都發(fā)生什么 }展開看每一輪實(shí)際干的事1. 新建一個(gè)StringBuilder2.append舊s的內(nèi)容3.append當(dāng)前i4. 調(diào)toString()生成新的 String5. 舊的s變成垃圾。也就是說第 n 輪要復(fù)制前 n-1 輪的所有字符??倧?fù)制量是 12...n ≈O(n2)。1 萬次循環(huán)臨時(shí)對(duì)象數(shù)以萬計(jì)GC 直接起飛。字節(jié)碼視角s i編譯后大致是new StringBuilder().append(s).append(i).toString()每次循環(huán)都new一次。這就是為什么慢。普通答法 vs 高分答法普通因?yàn)?String 不可變每次拼接都新建對(duì)象所以慢。高分我會(huì)把O(n2) 復(fù)制量和每輪 new StringBuilder 產(chǎn)生中間 String 垃圾講清楚并對(duì)比正確寫法只 new 一次、線性復(fù)制。量化之后考官一眼知道你真測(cè)過。三、編譯器到底做了什么優(yōu)化本篇第三道核心問答很多人把它和慢混為一談。面試官那編譯器對(duì)拼接不是有優(yōu)化嗎深度解析有但只覆蓋單條語句。javac在編譯期會(huì)把同一個(gè)表達(dá)式里的串自動(dòng)轉(zhuǎn)成StringBuilder.append的鏈?zhǔn)秸{(diào)用// 源碼 String s a b c; // 編譯后等價(jià)于 String s new StringBuilder().append(a).append(b).append(c).toString();注意這里只 new一次StringBuilder所以單條語句的性能沒問題別被String 慢的謠言嚇到。但循環(huán)里每次迭代是獨(dú)立語句編譯器沒法把跨迭代的拼接合并于是每輪各 new 一次——優(yōu)化在此失效。Android 側(cè)補(bǔ)充R8 / ProGuard 在編譯期還會(huì)做字符串常量折疊ab直接變成abKotlin 編譯器對(duì)字符串模板$a$b也是生成 append 鏈。但沒有任何編譯器能跨循環(huán)合并這是語義決定的。高分答法我會(huì)總結(jié)一句——優(yōu)化救得了單條語句救不了循環(huán)。判斷用不用 StringBuilder看的是拼接是否跨多次迭代而不是用了幾個(gè)加號(hào)。四、容量與擴(kuò)容真正的性能細(xì)節(jié)面試官那 StringBuilder 就一定快不注意容量也白搭吧。深度解析對(duì)。默認(rèn)容量只有16append 超出時(shí)會(huì)擴(kuò)容??碅bstractStringBuilder的擴(kuò)容邏輯int newCapacity (oldCapacity 1) 2; // 舊容量 *2 2 if (newCapacity - minCapacity 0) newCapacity minCapacity; value Arrays.copyOf(value, newCapacity); // 復(fù)制舊數(shù)組到新數(shù)組意思是超了就翻倍 2然后Arrays.copyOf把老數(shù)據(jù)整體拷過去。頻繁擴(kuò)容 頻繁數(shù)組拷貝。實(shí)戰(zhàn)場(chǎng)景快手客戶端里拼一段接口返回的 JSON 日志長(zhǎng)度可能上千字符。如果你new StringBuilder()默認(rèn) 16會(huì)經(jīng)歷 16→34→70→142→286→574→1150 多次擴(kuò)容拷貝。正確做法// 預(yù)估容量一步到位零擴(kuò)容 val sb StringBuilder(estimatedLen) for (item in list) sb.append(item.toLogLine())高分答法我會(huì)補(bǔ)一個(gè) Android 專屬點(diǎn)——TextView.setText(CharSequence)內(nèi)部大量用 Spannable/StringBuilder如果你在onBindViewHolder里反復(fù)拼長(zhǎng)文本且不設(shè)容量列表滑動(dòng)就會(huì)掉幀。這類細(xì)節(jié)性能組考官最愛聽。五、開放追問多線程到底用誰面試官多線程環(huán)境拼字符串該用 StringBuffer 嗎深度解析這題有陷阱。StringBuffer 確實(shí)線程安全但線程安全≠該用?,F(xiàn)代寫法更推薦局部變量每個(gè)線程自己的 StringBuilder根本不需要鎖最快。ThreadLocal復(fù)用緩沖區(qū)又避鎖。StringJoinerJava 8專門拼集合帶分隔符val j StringJoiner(,) list.forEach { j.add(it) } val s j.toString() // a,b,cKotlin 的buildString {}內(nèi)部就是StringBuilderDSL 寫法更爽val s buildString { repeat(1000) { append(it) } }高分答法我的看法是——除非你要在一個(gè)被多線程共享的同一緩沖區(qū)上并發(fā) append這種場(chǎng)景極少否則 StringBuffer 基本是歷史包袱。與其加鎖不如讓每個(gè)線程各拼各的、最后合并。這展現(xiàn)的不是 API 記憶是并發(fā)設(shè)計(jì)意識(shí)。收尾幾條能落地的建議三兄弟這道題快手想篩的是有沒有性能體感。String 不可變不是缺點(diǎn)是特性不是原罪循環(huán)里才是StringBuilder 不是萬能不設(shè)容量照樣拉胯。面試 Tips具體答法先講底層結(jié)構(gòu)char[] 緩沖區(qū) 默認(rèn)容量16再講差異邏輯最順。被問慢立刻分單條語句編譯器優(yōu)化vs 循環(huán)O(n2)兩情況別一刀切。提擴(kuò)容1 2和Arrays.copyOf拷貝展示你看過源碼。多線程題反手給 ThreadLocal / StringJoiner / buildString區(qū)分度拉滿。講真客戶端性能優(yōu)化八成都在這些不起眼的細(xì)節(jié)里。---點(diǎn)贊、在看、轉(zhuǎn)發(fā)三連是對(duì)我最大的支持「Android 大廠面經(jīng)·從入門到精通」連載系列上一篇第002篇 字節(jié)跳動(dòng)·應(yīng)屆 Android 面試——String 為什么設(shè)計(jì)成不可變下一篇預(yù)告第004篇 百度·初級(jí) Android 面試——equals 和 hashCode 的約定評(píng)論區(qū)聊聊你面試遇到過最難的 Android 問題是哪道關(guān)于本系列「Android 大廠面經(jīng)·從入門到精通」是360篇連載系列覆蓋美團(tuán)、字節(jié)跳動(dòng)、阿里巴巴、快手、百度、京東、華為、小米等30家公司從 Java 基礎(chǔ)到架構(gòu)師終面的真實(shí)面試內(nèi)容。本篇屬于階段1·入門篇——Java 語言基礎(chǔ)。