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