ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

快手·Android 开发面试——StringBuilder 和 StringBuffer 选错一个

快手·Android 开发面试——StringBuilder 和 StringBuffer 选错一个 快手客户端性能组面试有个习惯不考你多炫的架构先问字符串拼接。App 里有个接口要拼上千条日志你用还是 StringBuilder——看似送分答错的人能有一半。说白了String、StringBuilder、StringBuffer 三兄弟初级岗背定义快手要的是你真的在性能敏感路径上趟过坑。今天把底层结构、性能差异、编译器优化一次性讲清。一、三者底层到底差在哪面试官String、StringBuilder、StringBuffer 区别候选人标准答法String 不可变后两个可变StringBuffer 线程安全StringBuilder 不是。深度解析从存储结构看就明白了。StringJava 8 是private final char value[]Java 9 是byte[]紧凑字符串。不可变任何改都返回新对象。StringBuilder / StringBuffer内部都是一个可变的 char[] 缓冲区默认容量 16。append()往里填不够就扩容。区别只在——StringBuffer 的每个 public 方法都加了synchronizedStringBuilder 没有。// AbstractStringBuilder 的共同底层 char[] value; // 缓冲区 int count; // 已用长度普通答法 vs 高分答法普通StringBuffer 线程安全所以慢StringBuilder 快。高分我会点出线程安全的代价是每次方法调用都要拿锁即使单线程也在空转同步所以在明确单线程的场景StringBuffer 是纯粹的性能浪费。快手这种高并发客户端主线程拼字符串用 StringBuilder不要无脑 StringBuffer。有意思的是很多人不知道StringBuffer 的toString()在旧 JDK 也是synchronized的新版本优化掉了但方法级的锁开销仍在。二、为什么 拼接会慢性能真相本篇第二道核心问答也是这道题的灵魂。面试官那s x到底慢在哪深度解析关键看是不是在循环里。单条语句String s a b c;编译器会优化见第三组几乎没额外开销。但循环里的是灾难var s for (i in 0 until 10000) { s i // 每一轮都发生什么 }展开看每一轮实际干的事1. 新建一个StringBuilder2.append旧s的内容3.append当前i4. 调toString()生成新的 String5. 旧的s变成垃圾。也就是说第 n 轮要复制前 n-1 轮的所有字符。总复制量是 12...n ≈O(n²)。1 万次循环临时对象数以万计GC 直接起飞。字节码视角s i编译后大致是new StringBuilder().append(s).append(i).toString()每次循环都new一次。这就是为什么慢。普通答法 vs 高分答法普通因为 String 不可变每次拼接都新建对象所以慢。高分我会把O(n²) 复制量和每轮 new StringBuilder 产生中间 String 垃圾讲清楚并对比正确写法只 new 一次、线性复制。量化之后考官一眼知道你真测过。三、编译器到底做了什么优化本篇第三道核心问答很多人把它和慢混为一谈。面试官那编译器对拼接不是有优化吗深度解析有但只覆盖单条语句。javac在编译期会把同一个表达式里的串自动转成StringBuilder.append的链式调用// 源码 String s a b c; // 编译后等价于 String s new StringBuilder().append(a).append(b).append(c).toString();注意这里只 new一次StringBuilder所以单条语句的性能没问题别被String 慢的谣言吓到。但循环里每次迭代是独立语句编译器没法把跨迭代的拼接合并于是每轮各 new 一次——优化在此失效。Android 侧补充R8 / ProGuard 在编译期还会做字符串常量折叠ab直接变成abKotlin 编译器对字符串模板$a$b也是生成 append 链。但没有任何编译器能跨循环合并这是语义决定的。高分答法我会总结一句——优化救得了单条语句救不了循环。判断用不用 StringBuilder看的是拼接是否跨多次迭代而不是用了几个加号。四、容量与扩容真正的性能细节面试官那 StringBuilder 就一定快不注意容量也白搭吧。深度解析对。默认容量只有16append 超出时会扩容。看AbstractStringBuilder的扩容逻辑int newCapacity (oldCapacity 1) 2; // 旧容量 *2 2 if (newCapacity - minCapacity 0) newCapacity minCapacity; value Arrays.copyOf(value, newCapacity); // 复制旧数组到新数组意思是超了就翻倍 2然后Arrays.copyOf把老数据整体拷过去。频繁扩容 频繁数组拷贝。实战场景快手客户端里拼一段接口返回的 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里反复拼长文本且不设容量列表滑动就会掉帧。这类细节性能组考官最爱听。五、开放追问多线程到底用谁面试官多线程环境拼字符串该用 StringBuffer 吗深度解析这题有陷阱。StringBuffer 确实线程安全但线程安全≠该用。现代写法更推荐局部变量每个线程自己的 StringBuilder根本不需要锁最快。ThreadLocal复用缓冲区又避锁。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) } }高分答法我的看法是——除非你要在一个被多线程共享的同一缓冲区上并发 append这种场景极少否则 StringBuffer 基本是历史包袱。与其加锁不如让每个线程各拼各的、最后合并。这展现的不是 API 记忆是并发设计意识。收尾几条能落地的建议三兄弟这道题快手想筛的是有没有性能体感。String 不可变不是缺点是特性不是原罪循环里才是StringBuilder 不是万能不设容量照样拉胯。面试 Tips具体答法先讲底层结构char[] 缓冲区 默认容量16再讲差异逻辑最顺。被问慢立刻分单条语句编译器优化vs 循环O(n²)两情况别一刀切。提扩容1 2和Arrays.copyOf拷贝展示你看过源码。多线程题反手给 ThreadLocal / StringJoiner / buildString区分度拉满。讲真客户端性能优化八成都在这些不起眼的细节里。---点赞、在看、转发三连是对我最大的支持「Android 大厂面经·从入门到精通」连载系列上一篇第002篇 字节跳动·应届 Android 面试——String 为什么设计成不可变下一篇预告第004篇 百度·初级 Android 面试——equals 和 hashCode 的约定评论区聊聊你面试遇到过最难的 Android 问题是哪道关于本系列「Android 大厂面经·从入门到精通」是360篇连载系列覆盖美团、字节跳动、阿里巴巴、快手、百度、京东、华为、小米等30家公司从 Java 基础到架构师终面的真实面试内容。本篇属于阶段1·入门篇——Java 语言基础。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表