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/

面试考察点

  1. 基础概念掌握:面试官不仅仅是想让你回答一个数字,更是想考察你是否理解 Java 对象创建机制、字符串常量池的工作原理,以及字面量与 new 关键字的区别。

  2. 分情况分析能力:这道题的答案取决于字符串常量池的当前状态,考察你是否有 "条件思维",能否识别出不同的执行场景。

  3. 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

面试高频追问

  1. 追问一String s = "a" + "b" + "c" 创建了几个对象?

    在编译期,编译器会将 "a" + "b" + "c" 优化为 "abc",所以只会在常量池中创建 1 个对象。这是编译期常量折叠优化。

  2. 追问二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 个
  3. 追问三String.intern() 方法有什么用?

    intern() 方法会将字符串对象加入常量池(如果池中不存在),并返回常量池中的引用。可以用于手动优化,减少重复字符串的内存占用:

    String s1 = new String("hello").intern();
    String s2 = "hello";
    System.out.println(s1 == s2);  // true,都指向常量池
    
  4. 追问四: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 关键字的堆对象创建机制,生产环境应优先使用字面量方式避免不必要的对象创建。