String str = new String("abc") 创建了几个对象?
String str = new String("abc") 创建了几个对象?
zhangchangsheng面试考察点
- 基础概念掌握:面试官不仅仅是想让你回答一个数字,更是想考察你是否理解 Java 对象创建机制、字符串常量池的工作原理,以及字面量与
new关键字的区别。 - 分情况分析能力:这道题的答案取决于字符串常量池的当前状态,考察你是否有 “条件思维”,能否识别出不同的执行场景。
- JVM 内存模型理解:考察你是否清楚堆内存、字符串常量池的位置关系(JDK 6 vs JDK 7+ 的差异),以及对象在内存中的存储方式。
核心答案
这道题的答案 取决于字符串常量池中是否已存在 "abc": **** **** **** ****
| 场景 | 常量池状态 | 创建对象数 | 对象位置 |
|---|---|---|---|
| 首次执行 | 不存在 “abc” | 2 个 | 常量池 1 个 + 堆 1 个 |
| 非首次执行 | 已存在 “abc” | 1 个 | 仅堆中 1 个 |
一句话总结:new String("abc") 至少创建 1 个堆对象,最多创建 2 个对象(取决于常量池状态)。
深度解析
一、执行过程详解
flowchart TB
S1["步骤 1:解析字符串字面量 abc<br/>JVM 检查字符串常量池"] --> Q{"常量池中已有 abc?"}
Q -->|"没有"| C1["创建对象并放入池中(对象①)"]
Q -->|"有"| C2["直接复用池中对象"]
C1 --> S2["步骤 2:执行 new String()<br/>在堆内存中创建新的 String 对象(对象②)<br/>value 指向常量池中 abc 的字符数组"]
C2 --> S2
S2 --> S3["步骤 3:赋值<br/>str 引用指向堆中的 String 对象"]
上图展示了 String str = new String("abc") 的完整执行流程:
- 步骤 1(类加载阶段):JVM 首先解析字符串字面量
"abc",检查字符串常量池。如果常量池中不存在"abc",则创建一个 String 对象放入池中(对象①);如果已存在,则直接复用池中对象。 - 步骤 2(运行阶段):
new关键字触发堆内存分配,创建一个新的 String 对象(对象②)。这个对象的value数组引用会指向常量池中"abc"对应的字符数据。 - 步骤 3(赋值):变量
str指向堆中新创建的 String 对象,而不是常量池中的对象。
二、内存布局图解
flowchart TB
subgraph HEAP["JVM 堆内存(首次执行共创建 2 个对象)"]
subgraph POOL["字符串常量池"]
O1["String abc(对象①)"]
end
subgraph NORMAL["普通堆区"]
O2["String 对象(对象②)"]
end
end
STR["str 引用"] --> O2
OTHER["其他引用"] --> O1
上图清晰地展示了首次执行时的内存布局:
- 对象①(常量池中):由字符串字面量
"abc"触发创建,存储在字符串常量池中。这个对象可以被多个引用共享复用。 - 对象②(堆中):由
new String()显式创建,存储在普通堆区。这个对象是str变量独占的,不会被共享。 - 数据共享:虽然创建了两个 String 对象,但它们内部可能共享同一份字符数组数据(取决于 JDK 版本和具体实现),这就是为什么
str.equals("abc")返回true。
三、代码验证
public class StringObjectTest { |
代码执行分析:
- 第 1 行
new String("abc"):常量池无"abc",创建 2 个对象(常量池 1 + 堆 1) - 第 2 行
new String("abc"):常量池已有"abc",只创建 1 个对象(堆 1) - 第 3 行
"abc":直接复用常量池对象,0 个新对象
四、JDK 版本差异
flowchart TB
subgraph JDK6["JDK 6 及之前"]
PG["方法区(PermGen)"] --> P1["字符串常量池:abc"]
H1["Java 堆"] --> O1["new String() 创建的对象"]
end
P1 --> PROB["问题:PermGen 空间有限,大量字符串易导致 OOM"]
PROB --> H2
subgraph JDK7["JDK 7 及之后"]
H2["Java 堆"] --> P2["字符串常量池:abc"]
H2 --> O2["new String() 创建的对象"]
end
P2 --> ADV["优势:堆空间更大,GC 可回收无用字符串"]
上图对比了不同 JDK 版本中字符串常量池的位置差异:
- JDK 6 及之前:字符串常量池位于永久代,与堆内存分离。永久代空间固定且较小,大量字符串容易导致
java.lang.OutOfMemoryError: PermGen space。 - JDK 7 及之后:字符串常量池被移到 Java 堆中。堆空间通常更大且可动态扩展,GC 可以回收不再引用的字符串对象,大大减少了 OOM 风险。
- 对象创建数量不变:虽然常量池位置变了,但
new String("abc")创建对象的数量规则不变,仍然是”首次 2 个,非首次 1 个”。
五、最佳实践
避免不必要的 new String():
// ❌ 不推荐:浪费内存,创建多余对象 |
何时需要 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(); |
- 追问四: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 关键字的堆对象创建机制,生产环境应优先使用字面量方式避免不必要的对象创建。
声明:本文转载自小哈的博客 《String str = new String(“abc”) 创建了几个对象?》,版权归原文作者所有。转载请保留本声明与原文链接。
评论
匿名评论隐私政策
WalineTwikoo
