面试圈里有一道经久不衰的送分题也是送命题String str new String(abc)创建了几个对象我面过不少候选人从刚毕业的应届生到号称三年经验的开发十个人里能把这个答案讲清楚的撑死两三个。大多数人憋出一句两个对象然后面试官再追问一句为什么是两个在哪两个地方就卡壳了。这道题之所以经典是因为它背后串联着String 常量池、编译期优化、堆内存分配、引用指向关系四个核心知识点一道题就能把一个人的 Java 基本功摸得七七八八。这篇博文我就从面试官的角度把这道题掰开揉碎了讲一遍顺便把面试官接下来会追问的一连串String问题也一并拆给你看。无论你是准备面试还是单纯想把基础补扎实这篇文章都值得你耐心读完。1. 这道面试题到底在考什么先别急着背诵答案。你要明白面试官抛出这道题时他嘴上问的是创建了几个对象脑子里真正想确认的是下面这五件事你知不知道字符串对象和普通对象在内存分配上的差别。你理不理解 Java 的常量池机制尤其是字符串常量池的位置和生命周期。你能不能分清楚编译期和运行期两件完全不同的事情。你知不知道new关键字一定会触发堆上对象的创建而字面量赋值不一定。你能不能把创建了几个对象和栈上的引用指向谁这两件事分开说清楚。很多候选人只知道背结论创建了两个对象。但实际上这个答案只在一种情况下成立。如果你的条件变了答案也会跟着变。这就是为什么我强调学这道题不要背要理解它背后的机制。1.1 为什么面试官偏爱字符串问题字符串在 Java 里几乎是所有业务代码里出现频率最高的对象类型。日志、参数传递、JSON序列化、SQL拼接哪样离得开 String正因为太常用了很多开发反而忽略了对 String 底层机制的深究。面试官问字符串成本低、区分度高一个问题就能知道你是用过还是理解过。我自己的经验是能清晰回答这道题的人通常对 JVM 内存模型也有一定了解因为字符串常量池从 JDK 7 开始从方法区挪到了堆中这个细节会直接影响你对对象创建了几个的判断。反过来对这块一知半解的人后面追问intern()、字符串拼接优化基本也是答不上来的。1.2 一个答案在不同条件下会变这是我给所有准备面试的人的第一个提醒String str new String(abc)这道题没有一个恒定不变的答案。它的完整回答是如果字符串常量池中还没有abc这个字符串那会创建两个对象一个在字符串常量池中编译期创建一个在堆上运行期new创建。如果字符串常量池中已经存在abc那只会创建一个对象也就是new出来的堆上对象常量池里那个是直接复用。这个看情况的答案才是面试官真正想听到的完整版。你上来直接说两个说明你背过答案但没理解机制你把上述两种情况都讲清楚再补一句引用指向这题才算答到位了。2. 字符串常量池理解这道题的第一块拼图要理解这道题你首先得搞清楚字符串常量池到底是个什么东西。很多人对它的理解停留在一块存放字符串的内存区域这没错但太模糊了。我尽量讲得接地气一点。2.1 常量池里的字符串到底怎么来的Java 源码文件经过编译后会生成 class 文件class 文件里有一个**常量池Constant Pool**区域里面存放了类、方法、字段、字符串字面量等符号信息。也就是说你在代码里写的每一个字符串字面量比如abc在编译期就已经被记录在 class 文件的常量池中了。当 JVM 加载这个 class 文件时这些字符串字面量会被解析并放入运行时常量池进而存放到 JVM 的**字符串常量池String Pool**里。所以记住一个关键点字面量字符串是编译期就确定下来的它在类加载阶段就会进入字符串常量池。你可能会问字符串常量池到底在哪里这个问题的答案在不同 JDK 版本里不一样。JDK 6 及之前字符串常量池放在方法区永久代里JDK 7 开始它被移到了 Java 堆中。这也是为什么有些老面试题会说常量池在方法区放在今天这个说法已经不准确了。如果你面试时能顺带提一句这个 JDK 版本的变化面试官对你的印象分会明显不一样。2.2 字符串为何特殊不可变与复用String 类被设计成不可变对象也就是对象一旦创建它的值就不能被修改。这种设计带来的一个好处是安全性高多个引用可以放心共享同一个字符串对象而不用担心被篡改另一个好处就是为字符串池化复用创造了条件。你可以把字符串常量池想象成一个公共仓库所有用字面量方式创建的字符串都会先到这个仓库里找如果仓库里已经有了相同内容的字符串就直接返回引用不再重复创建。这就是为什么下面这段代码打印的结果是trueString s1 abc; String s2 abc; System.out.println(s1 s2); // trues1和s2指向的是字符序列完全相同的同一个字符串常量池对象比较的是引用地址地址相同所以是true。这跟equals比较内容根本不是一回事很多新手会把它们搞混。2.3 基本类型包装类也有池别搞混顺着这个思路面试官还经常会考一道变种题Integer a 127; Integer b 127; a b结果是true而Integer c 128; Integer d 128; c d结果是false。这就是基本类型包装类的缓存机制Integer默认缓存了-128到127之间的值超出这个范围就是两个不同的对象了。它跟字符串常量池不是一回事但考察的都是同一个底层能力你是不是真的理解对象在内存中怎么分配和复用。能把这两道题串联起来回答的人基础一定不会差。3. String str new String(abc) 逐层拆解现在正式拆这道题。你要把一整行代码拆成编译期和运行期两半来看答案自然就浮出水面了。3.1 编译期的abc发生了什么你在代码中写下的abc是字符串字面量编译成 class 文件后它作为常量池中的一个CONSTANT_String_info条目存在。JVM 加载类时会把这个字面量放进字符串常量池。这一步发生在编译期和类加载阶段跟你有没有写new没有任何关系。这里要特别注意一个点只要你的代码里写了abc这个字面量哪怕你只写了一行String x abc它也会在类加载时进入常量池。所以面试官如果追问new String(abc)里那个abc是不是也需要创建对象答案是它本来就会以字面量的形式进入常量池跟你new不new没关系。3.2 运行期 new String 又发生了什么接下来看new String(abc)这一部分。new关键字在运行期的语义非常明确在堆上分配一块内存创建一个全新的 String 对象。这个对象的内容虽然也是abc但它和常量池里的那个abc是两个完全独立的对象只是内容恰好一样。new出来的这个 String 对象它的内部字符数组char[]JDK 9 之后是byte[]会被赋值为常量池中那个字符串的内容。所以准确地说new String(abc)是基于常量池中已有的字符串内容在堆上再拷贝创建了一个新对象。3.3 最终答案创建了几个对象把编译期和运行期合在一起看如果常量池中没有abc编译期/类加载阶段创建 1 个常量池对象运行期new创建 1 个堆对象。共 2 个对象。如果常量池中已有abc常量池对象已存在无需重复创建运行期new创建 1 个堆对象。共 1 个对象。最后str这个引用变量指向哪里它指向的是堆上new出来的那个对象不是常量池里的那个。这一点也经常被面试官追问你要一口答出来str的引用指向的是堆内存中的 String 对象。为了帮你彻底看清这个过程我用一段代码做个对比演示String str1 new String(abc); // 假设常量池没有abc String str2 abc; // 直接复用常量池对象 String str3 str1; // 指向堆上同一个对象 System.out.println(str1 str2); // false堆对象 vs 常量池对象 System.out.println(str1 str3); // true同一个堆对象 System.out.println(str2 abc); // true两个字面量都指向常量池同一个对象str1 str2是false这个结果是全题的核心所在它证明了new出来的字符串对象和常量池中的对象不是同一个东西。很多候选人能说出创建两个对象但解释不了为什么str1 str2是false这就是理解还差最后一层窗户纸。3.4 面试官的高频追加提问如果这题你答得漂亮面试官通常会追加下面这几个问题我帮你提前演练一下问String s ab cd创建了几个对象答ab和cd分别是两个常量池对象但ab cd这个表达式在编译期就会被优化成abcd这个单一字面量所以编译期创建了ab、cd、abcd三个常量池对象假设之前均不存在运行期没有new不产生堆对象。如果面试官问的是创建了几个对象这个问题反而比new String(abc)更阴险因为常量折叠会导致ab cd和abcd是同一个对象。问String s new String(ab) new String(cd)创建了几个对象答这题比上一题复杂得多。ab和cd两个字面量在常量池中创建假设不存在两个new在堆上创建两个 String 对象new String(ab) new String(cd)底层是通过StringBuilder完成的StringBuilder在toString()时会new一个 String 对象。所以至少创建了 5 个对象常量池 2 个两个new出来的 String 各 1 个拼接结果 1 个。当然StringBuilder这个临时对象自己也算一个就看面试官怎么定义对象的范围了。这种题考察的就是你对字符串拼接底层实现的掌握程度一般问到这里候选人基本就招架不住了。问String s abc; s s def;这行代码创建了几个对象答同样要看常量池情况。abc已在常量池假设运行时s def会创建一个StringBuilder追加def后toString()生成新的 String 对象。所以运行期至少创建了 StringBuilder 和新 String 两个对象。这题的核心考点是字符串常量拼接在编译期完成字符串变量拼接在运行期通过 StringBuilder 完成。4. 从这道题延伸出去的 String 全家桶考点面试官问完创建个数之后话题往往会顺势滑向 String 的兄弟类和方法细节。这些都是我在面试中真实问过、也是后来者最容易被问懵的地方索性一次讲清楚。4.1 String、StringBuffer、StringBuilder 三兄弟怎么选很多候选人能把三者的区别背得滚瓜烂熟String 不可变StringBuffer 线程安全StringBuilder 线程不安全。但你再追问一句那 StringBuffer 的线程安全是怎么实现的一半人就愣住了。StringBuffer的核心方法是加了synchronized关键字的所以多线程环境下对同一个 StringBuffer 实例的修改是串行化的这就是它线程安全的由来。StringBuilder没有加锁所以在单线程下性能更高因为没有同步开销。日常开发中方法内局部变量的字符串拼接用 StringBuilder 足够涉及多线程共享可变字符串才用 StringBuffer。我见过太多因为这里概念不清而翻车的情况。有个候选人答得特别逗StringBuffer 是线程安全的所以我们应该优先用 StringBuffer。这个说法放在单线程场景完全是多余的你把锁的开销白白背在身上图什么呢面试官想听到的是你有场景意识而不是死记概念。关于StringBuffer 转换为 String我顺便多说一句这是很常见的实际需求。StringBuffer有个toString()方法直接调用就能得到 StringStringBuffer buffer new StringBuffer(hello); String result buffer.toString();底层实现上toString()会基于当前 StringBuffer 内部的字符数组创建一个新的 String 对象。同理StringBuilder也是这么转的。我在项目里经常看到有人用buffer.toString()之后忘了接收返回值然后奇怪为什么 buffer 没有变成 String——因为 toString 返回的是新对象不是把原对象改造成 StringStringBuffer 永远是 StringBuffer。4.2 String 类的常用方法面试常考这几个接下来盘点一下 String 类的高频方法。这些不是面试中直接考创建对象而是在写代码的环节会用到的面试官也常会假装不经意地问一句length()返回字符串长度。注意是length()方法不是length属性数组才有length属性新手最容易在这两个概念上犯迷糊。charAt(int index)返回指定位置字符。面试中经常配合字符串反转、回文判断这类手写算法出现。substring(int beginIndex, int endIndex)截取子串。JDK 7 之后底层是新建字符数组不再共享原字符串的char[]这也是一个经典的内存优化考点。equals()与equalsIgnoreCase()比较字符串内容是否相同。任何两个字符串对象之间比较内容都该用equals永远是在比较引用地址。indexOf()/lastIndexOf()查找字符或子串首次/最后一次出现的索引。replace()/replaceAll()替换字符或正则匹配的子串。split(String regex)按正则表达式拆分字符串。这里有个大坑split(.)是拆不开的因为.在正则里是任意字符必须写成split(\\.)。trim()/strip()去掉首尾空格。JDK 11 引入的strip()不仅能去除半角空格还能去除全角空格等 Unicode 空白字符比trim()更彻底。intern()这个方法单独拎出来说它能把堆上的字符串对象手动放入常量池并返回常量池中的引用。我见过一个很典型的面试场景面试官让候选人写一个方法统计字符串中每个字符出现的次数。候选人用split循环切写出来的代码又慢又绕另一个候选人直接用 HashMap 遍历charAt()几行就搞定了。这就是基础方法熟练度的差距你 API 用得不熟连简单需求都写不利索。4.3 intern() 方法的正确打开方式intern()值得单独一节来讲因为它几乎是new String(abc)这道题的题眼。很多候选人不知道intern()是干嘛的但面试官只要问到了机制层面intern()基本必聊。intern()的行为是这样的当你在堆上的 String 对象上调用intern()时JVM 会检查字符串常量池里有没有相同内容的字符串。如果有直接返回常量池中的引用如果没有把这个字符串的内容复制到常量池中JDK 7 及之后可以在常量池中记录堆中对象的引用并不一定要复制整个对象然后返回常量池中的引用。来看一段经典代码String s1 new String(abc); String s2 s1.intern(); String s3 abc; System.out.println(s1 s3); // false System.out.println(s2 s3); // trueintern()返回的是常量池引用s1是堆对象s3是常量池对象所以比较是false。s2是intern()的返回值指向常量池中的同一个对象跟s3是true。我实际项目中用过intern()的一次经历是做大量重复字符串的去重缓存。当时有个接第三方数据的服务单条数据里有一个状态字段取值就五六个固定值但每天要处理几百万条记录。如果不做处理每条记录都会产生一个新的 String 对象堆内存压力很大。后来在写入缓存之前对这个字段调了intern()让相同内容的字符串复用同一个常量池对象内存占用肉眼可见地降了下来。不过这里我必须给个忠告intern()不是万能的滥用它反而会搞垮你的 JVM。JDK 7 之后常量池在堆里如果无限制地把运行时产生的大量不同字符串intern()进去常量池会越撑越大最终可能触发堆溢出。所以intern()只适合用在有限集合、高重复率的字符串上比如状态值、枚举值、字典项而不适合用户输入类的内容。我自己做技术方案时会先估算字符串的基数基数超过几千就放弃intern()方案改用自定义的弱引用缓存这样既能复用对象又不会被常量池长期占住内存。5. 真实面试场景与避坑清单这一节我想直接模拟一遍真实面试现场把常见的对话节奏和回答话术写出来顺便盘点一下我在面试官视角看到的高频错误。5.1 典型追问链与参考回答话术我一般会按下面这条链路追问Q1String str new String(abc) 创建了几个对象 Q2你刚才说常量池和堆这两个对象分别在什么时候创建的 Q3str 这个引用指向的是哪个对象 Q4如果是 String s abc创建了几个对象 Q5String s a b c 创建了几个对象 Q6你平时代码里大量拼接字符串用的是什么 Q7StringBuilder 和 StringBuffer 有什么区别完整的回答话术我给你整理了一段面试时可以照着这个思路组织语言这行代码创建的对象数量取决于abc这个字面量是否已经存在于字符串常量池中。如果不存在会创建两个对象一个是编译期生成的、类加载时放入字符串常量池的对象另一个是运行期 new 关键字在堆上创建的对象str 引用指向堆上的这个对象。如果常量池里已经有abc那就只创建一个堆对象常量池中的对象直接复用。String 的赋值和 new 的区别在于字面量赋值会去常量池中寻找或创建对象而 new 无论如何都会在堆上创建一个新对象。这段话如果你能不打磕绊地说完整这道题在面试官那里至少是个良以上的评价。5.2 我在面试官视角看过的翻车现场我总结了一些高频翻车点帮你提前避坑。第一个翻车点直接答两个对象。这个答案本身在特定条件下没错但暴露了你没有考虑常量池里可能已有abc的情况。面试官听到这个答案后通常会追问一句一定有两个吗然后等你挣扎。你要做的就是在第一遍回答时就把两个情况都讲清楚而不是等追问再补。第二个翻车点分不清编译期和运行期。有人会说创建了三个对象abc 一个new 一个str 一个这就把引用变量也算成对象了属于概念混淆。引用变量 str 是在栈上分配的引用不是对象本身它只是指向对象的地址。第三个翻车点答非所问扯到常量折叠。候选人听到new String(abc)之后上来就背一段JVM 会先解析常量池然后用 StringBuilder 拼接之类的答案完全偏离了题目。这道题里根本没有拼接你把题目听错了。第四个翻车点不知道 JDK 版本影响了常量池位置。候选人答完对象在方法区之后我追问一句那 JDK 7 之后呢就沉默的不在少数。基础知识点必须落到版本的变化线上才能证明你是真的在关注这门语言的发展而不是背了一本有年代感的老旧面经。5.3 面试之外的工程启示别在生产代码里放纵 new String把话题拉回工程实践。这道题给我们的最大启示其实不在面试里而在日常编码习惯上。我 review 同事代码时偶尔会看到有人写String s new String(固定值)。这种写法除了多创建一个堆对象之外没有任何价值纯属浪费内存。正确的写法就是直接String s 固定值让 JVM 走常量池复用。虽然单个对象的内存开销微乎其微但如果是高频执行路径里的垃圾生成GC 压力就会积少成多。另外循环里拼接字符串是最常见的性能杀手。看这段代码String result ; for (int i 0; i 1000; i) { result result i ,; }每次循环都会创建一个新的 StringBuilder、调用两次 append最后toString()生成新 String。1000 次循环就是 1000 个中间 String 对象GC 忙得团团转。正确的写法是StringBuilder sb new StringBuilder(4096); for (int i 0; i 1000; i) { sb.append(i).append(,); } String result sb.toString();我还会建议你在创建 StringBuilder 时尽量预估初始容量。如果你大概知道结果的长度直接new StringBuilder(长度)可以减少扩容次数。StringBuilder 内部默认容量是 16超过 16 会自动扩容扩容涉及数组复制这也是开销。字节码层面号拼接字符串时javac 编译器会自动把代码重写成 StringBuilder 的 append 调用。所以我前面说字符串变量拼接在运行期通过 StringBuilder 完成不是类比而是编译器的真实行为你用javap -c反编译一下就能看到。5.4 最后分享一个我常用的记忆技巧这道题我讲给带过的每一位新人后来总结成一个傻瓜式记忆口诀分享给你字面量进池new 必上堆编译期管家运行期堆内引用指谁看你在哪种场合。展开说一下字面量字符串编译期就进了常量池new一定在堆上创建新对象常量池创建属于编译期和类加载阶段的事堆对象创建属于运行期引用变量指向谁要看具体代码new String(abc)的引用指向堆对象abc字面量赋值的引用指向常量池对象。这个口诀不是让你背答案而是帮你快速建立画面感。每当你在面试中碰到类似的字符串问题先在脑海里画出常量池和堆两个区域再往里面放对象回答立刻有了层次。我试过用这个方法给不少准入职的同事做过模拟面试通过率确实高了不少。回到最开始那道题你现在应该能给出一个让面试官满意的完整答案了。不过比答案更重要的是你借这道题把 JVM 内存分配、编译期运行期分工、String 不可变设计、字符串拼接底层实现这些知识点串成了一条线。面试问到这儿你已经不是在应付问题了而是在跟面试官平等地聊技术。这才是这道题真正想让你练成的东西。