String str = new String(“abc”) 创建了几个对象?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
基础概念掌握:面试官不仅仅是想让你回答一个数字,更是想考察你是否理解 Java 对象创建机制、字符串常量池的工作原理,以及字面量与
new关键字的区别。 -
分情况分析能力:这道题的答案取决于字符串常量池的当前状态,考察你是否有 "条件思维",能否识别出不同的执行场景。
-
JVM 内存模型理解:考察你是否清楚堆内存、字符串常量池的位置关系(JDK 6 vs JDK 7+ 的差异),以及对象在内存中的存储方式。
核心答案
这道题的答案 取决于字符串常量池中是否已存在 "abc":
| 场景 | 常量池状态 | 创建对象数 | 对象位置 |
|---|---|---|---|
| 首次执行 | 不存在 "abc" | 2 个 | 常量池 1 个 + 堆 1 个 |
| 非首次执行 | 已存在 "abc" | 1 个 | 仅堆中 1 个 |
一句话总结:new String("abc") 至少创建 1 个堆对象,最多创建 2 个对象(取决于常量池状态)。
深度解析
一、执行过程详解
上图展示了 String str = new String("abc") 的完整执行流程:
-
步骤 1(类加载阶段):JVM 首先解析字符串字面量
"abc",检查字符串常量池。如果常量池中不存在"abc",则创建一个 String 对象放入池中(对象①);如果已存在,则直接复用池中对象。 -
步骤 2(运行阶段):
new关键字触发堆内存分配,创建一个新的 String 对象(对象②)。这个对象的value数组引用会指向常量池中"abc"对应的字符数据。 -
步骤 3(赋值):变量
str指向堆中新创建的 String 对象,而不是常量池中的对象。
二、内存布局图解
上图清晰地展示了首次执行时的内存布局:
-
对象①(常量池中):由字符串字面量
"abc"触发创建,存储在字符串常量池中。这个对象可以被多个引用共享复用。 -
对象②(堆中):由
new String()显式创建,存储在普通堆区。这个对象是str变量独占的,不会被共享。 -
数据共享:虽然创建了两个 String 对象,但它们内部可能共享同一份字符数组数据(取决于 JDK 版本和具体实现),这就是为什么
str.equals("abc")返回true。
三、代码验证
public class StringObjectTest {
public static void main(String[] args) {
// 情况 1:首次执行,常量池无 "abc"
String s1 = new String("abc");
// 情况 2:非首次执行,常量池已有 "abc"
String s2 = new String("abc");
// 验证:s1 和 s2 是不同对象(都在堆中)
System.out.println(s1 == s2); // false
// 验证:s1 和常量池中的 "abc" 是不同对象
System.out.println(s1 == "abc"); // false
// 验证:字面量创建指向常量池
String s3 = "abc";
System.out.println(s3 == "abc"); // true
// 验证:内容相同
System.out.println(s1.equals(s3)); // true
}
}
代码执行分析:
- 第 1 行
new String("abc"):常量池无"abc",创建 2 个对象(常量池 1 + 堆 1) - 第 2 行
new String("abc"):常量池已有"abc",只创建 1 个对象(堆 1) - 第 3 行
"abc":直接复用常量池对象,0 个新对象
四、JDK 版本差异
上图对比了不同 JDK 版本中字符串常量池的位置差异:
-
JDK 6 及之前:字符串常量池位于永久代,与堆内存分离。永久代空间固定且较小,大量字符串容易导致
java.lang.OutOfMemoryError: PermGen space。 -
JDK 7 及之后:字符串常量池被移到 Java 堆中。堆空间通常更大且可动态扩展,GC 可以回收不再引用的字符串对象,大大减少了 OOM 风险。
-
对象创建数量不变:虽然常量池位置变了,但
new String("abc")创建对象的数量规则不变,仍然是"首次 2 个,非首次 1 个"。
五、最佳实践
避免不必要的 new String():
// ❌ 不推荐:浪费内存,创建多余对象
String s1 = new String("hello");
String s2 = new String("hello"); // 又创建一个堆对象
// ✅ 推荐:直接使用字面量,复用常量池对象
String s3 = "hello";
String s4 = "hello"; // 直接复用常量池中的对象,0 个新对象
// ✅ 特殊场景:确实需要独立对象时才使用 new
// 例如:需要独立 identity 的情况
何时需要 new String():
- 需要创建内容相同但引用不同的对象(极少见)
- 某些框架或库的特定需求
- 动态构建的字符串(但更推荐使用
StringBuilder)
面试高频追问
-
追问一:
String s = "a" + "b" + "c"创建了几个对象?在编译期,编译器会将
"a" + "b" + "c"优化为"abc",所以只会在常量池中创建 1 个对象。这是编译期常量折叠优化。 -
追问二:
String s = new String("a") + new String("b")创建了几个对象?会创建 6 个对象:
- 常量池:
"a"、"b"、"ab"(JDK 8 及之前可能不创建 "ab")共 2-3 个 - 堆:
new String("a")、new String("b")、StringBuilder.toString()创建的 String 对象,共 3 个
- 常量池:
-
追问三:
String.intern()方法有什么用?intern()方法会将字符串对象加入常量池(如果池中不存在),并返回常量池中的引用。可以用于手动优化,减少重复字符串的内存占用:String s1 = new String("hello").intern(); String s2 = "hello"; System.out.println(s1 == s2); // true,都指向常量池 -
追问四:JDK 7 之后
intern()有什么变化?JDK 7 之前,
intern()会在永久代创建新对象;JDK 7 之后,如果常量池中没有该字符串,intern()会保存堆中对象的引用,而不是再创建一个新对象。
常见面试变体
- "
String s = new String("xyz")创建了几个 String Object?" - "
String s = "hello"和String s = new String("hello")有什么区别?" - "以下代码创建了几个对象?
String s1 = "a"; String s2 = "b"; String s3 = s1 + s2;"
记忆口诀
对象数量判断:先看池,再看堆。池中没有先入池(1 个),堆中必 new(1 个),首次 2 个非首次 1 个。
总结
String str = new String("abc") 创建对象的数量取决于常量池状态:首次执行创建 2 个对象(常量池 1 + 堆 1),非首次执行只创建 1 个对象(仅堆中)。核心是理解字符串字面量的常量池机制和 new 关键字的堆对象创建机制,生产环境应优先使用字面量方式避免不必要的对象创建。