摘要本文围绕美团等大厂高频出现的经典面试题String s new String(111)展开先纠正题干里常见的单引号笔误再沿着 JVM 内存模型、字符串常量池演进、String 底层数据结构变革、字节码指令、构造器源码五个维度逐层拆解这道题的完整执行过程。文章会给出分 JDK 版本、分常量池状态、分统计口径的精确对象计数并通过 javap、JOL、MAT/OQL 等工具验证结论最后提供面试高分回答策略与多道变体题解析。读完本文你不仅能回答出“两个对象”还能解释清楚什么时候不是两个以及为什么在 JDK 8 与 JDK 11 下结论会有细节差异。一、先纠错单引号是这道题的第一道陷阱很多面试题整理的题干写的是String s new String(111)用的是一对单引号。严格来说这段代码在 Java 里根本无法通过编译。在 Java 语法中单引号表示char类型的字符字面量而且单引号内部只能放一个字符。例如char c 1;是合法写法但是char c 111;会直接报错编译器通常会提示 unclosed character literal 或者 too many characters in character literal。退一步说即使我们把它理解成new String(1)这段代码同样无法编译因为 String 类并没有提供接收单个char参数的构造器。String 的构造器要么接收char[]、byte[]要么接收另一个 String要么接收 StringBuilder、StringBuffer 等类型但就是不接收裸的char单字符。因此这道美团面试题的标准题意应当是双引号版本String s new String(111);本文后续全部内容都围绕这个双引号版本展开。如果面试时你发现题干写的是单引号可以直接礼貌地指出来这不但不会扣分反而是一个不错的加分点说明你真正理解了 Java 字符字面量与字符串字面量的区别。这个细节我们会在第十二章的误区清单中继续讨论。二、面试标准答案与它的不严谨如果去搜索这道题的答案最常看到的一句话是创建了两个对象一个是字符串常量池中的111另一个是堆中的String对象。这个答案在面试场景下算及格但它并不严谨。因为它默认了三个前提字符串常量池中此前没有111我们只统计 String 对象不统计底层数组对象我们讨论的是 JDK 8 及之前的经典版本。一旦面试官继续追问“如果常量池里已经有111呢”“底层那个 char 数组算不算对象”“JDK 9 之后 String 底层改成 byte 数组了答案会变吗”很多依赖死记硬背的候选人就会卡住。真正要讲清楚这道题必须理解四件事字符串字面量是如何、在什么时候进入字符串常量池的new关键字和 String 构造器分别做了什么String 底层数组在 JDK 8 和 JDK 9 之后的变化我们统计的对象口径是什么。下面我们一步步展开先从一个最基础的问题开始对象到底创建在 JVM 的哪个区域。三、JVM 内存模型与字符串常量池要理解对象创建数量首先要清楚对象创建发生在 JVM 的哪个区域。这道题涉及堆、方法区、运行时常量池、字符串常量池四个概念。很多误读都源于没有把这四层关系理清楚。3.1 运行时数据区一瞥JVM 把运行时内存划分为若干区域。与本题直接相关的主要有两个堆所有通过new创建的对象和数组都分配在堆上。堆是垃圾收集器管理的主要区域绝大多数对象的创建与回收都发生在这里。方法区存储类结构信息、常量、静态变量、即时编译后的代码等。在 JDK 8 之前方法区的实现叫永久代JDK 8 开始永久代被移除改由本地内存中的元空间承接类元数据。此外还有虚拟机栈、本地方法栈、程序计数器等区域它们和本题的对象计数关系不大这里不展开。一个需要特别说明的概念是对象本身一定分配在堆上。无论字符串常量池在哪个年代位于哪个区域池中保存的都只是“引用”真正的 String 实例始终在堆中。3.2 什么是运行时常量池每个类被编译后生成的 class 文件中都有一个常量池通常称为 Class 常量池里面存放字面量和符号引用。类被加载到 JVM 之后Class 常量池中的内容会被解析、转换形成运行时常量池。其中有一类常量非常重要就是CONSTANT_String_info。每当你在源码中写下一个字符串字面量例如111编译器就会在 Class 常量池里生成一个CONSTANT_String_info项它内部指向一个CONSTANT_Utf8_info项后者保存了 UTF-8 编码的字符串内容。但 Class 常量池里的CONSTANT_String_info本身还不是字符串对象。它只是一份静态描述表示“这个类里用到了字符串 111”。真正的 String 实例要等到类加载解析阶段以及运行期执行相关字节码指令时才会被创建并放入字符串常量池。3.3 字符串常量池 StringTable字符串常量池在 HotSpot 中的底层实现是一个全局的哈希表称为StringTable。它用来缓存字符串字面量保证相同的字面量在 JVM 中只保留一份从而实现字符串字面量的复用。它的两个关键特性必须搞清楚去重池中不会存在两个内容相同、分别独立的字面量 String 对象。相同内容的字面量只会指向同一个 String 实例。缓存引用StringTable 里保存的是 String 对象的引用而不是字符串内容本身。真正的 String 对象还是在堆上。这个“保存引用”的细节非常关键。很多人误以为字符串常量池里存的是字符序列本身其实不是。池子里是一个个指向实际 String 对象的引用。这也是为什么从 JDK 7 开始把 StringTable 从永久代挪到堆之后字符串常量池本身不再像以前那样容易导致永久代溢出。3.4 字符串常量池的三次搬迁字符串常量池的位置在 JDK 演进过程中发生过多次变化这也是这类面试题经常追问的点。下面按照版本线梳理一下。JDK 6 及以前永久代在这个时期字符串常量池位于永久代中。永久代是方法区的实现有固定上限默认容量不大。如果程序里调用了大量intern()或者存在大量字符串字面量很容易触发java.lang.OutOfMemoryError: PermGen space。JDK 7搬迁到堆JDK 7 把字符串常量池从永久代移到了堆中。这样做的目的是减少永久代的内存压力同时让字符串常量池的内存可以像普通堆对象一样被垃圾收集器更灵活地回收。正因为池子里存的是引用而引用指向的对象本来就在堆里所以这次迁移在语义上是比较自然的。JDK 8 及以后元空间接管类元数据字符串常量池留在堆JDK 8 彻底移除了永久代改用元空间存储类元数据。需要注意的是字符串常量池并没有跟着进入元空间而是继续留在堆里。运行时常量池中的其他内容随类元数据进入元空间但 StringTable 仍然在堆上。这三次位置变化直接影响了一些intern()题目的答案我们在第九章会详细讨论。四、String 底层结构的两次变革搞清楚 String 内部如何存储字符是精确计数对象的前提。很多人只知道 String 底层有个 char 数组却不知道从 JDK 9 开始它已经改成 byte 数组了。这个变化会轻微影响“底层数组算不算对象”的讨论但对面试题的核心结论影响不大反而是一个展示技术敏感度的好素材。4.1 JDK 8 及之前char[] value在 JDK 8 及之前的版本中String 的核心字段大致如下public final class String implements java.io.Serializable, ComparableString, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; }每个 String 对象内部都持有一个char[] value数组长度就是字符串的字符个数。111对应一个长度为 3 的 char 数组元素分别是1、1、1。4.2 JDK 9 及之后byte[] value coderJDK 9 引入了 Compact Strings 优化。由于绝大多数字符串都是 Latin-1 字符用一个字节就能表示而原来的 char 需要两个字节存在明显内存浪费。于是 String 底层换成了 byte 数组并新增一个coder字段来标记编码方式。public final class String implements java.io.Serializable, ComparableString, CharSequence, Constable, ConstantDesc { Stable private final byte[] value; private final byte coder; private int hash; }coder的值只有两种LATIN1或UTF16。如果字符串中所有字符都能用 Latin-1 表示就使用 LATIN1每个字符占 1 个字节否则使用 UTF16每个字符占 2 个字节。像111这样的纯数字 ASCII 字符串自然会使用 LATIN1 编码底层是一个长度为 3 的 byte 数组。4.3 不可变性从何而来String 之所以不可变主要有三重保障value字段用final修饰数组引用一旦初始化就不能被重新指向新的数组String 类本身是final的不能被继承避免了子类改写行为所有可能修改字符串内容的方法返回的都是新创建的 String 对象而不是原地修改底层数组。不过需要特别注意final char[] value只能保证value这个引用不被重新赋值并不能保证数组元素不被修改。真正的安全来自 String 内部从不提供修改数组元素的出口。如果通过反射强行拿到这个数组还是可以改的这正是反射破坏 String 不可变性的经典话题但不是本文重点我们只需理解“不可变”的边界在哪里即可。4.4 String(String original) 构造器的秘密这是本题最核心的源码。在 JDK 8 中它的实现非常短public String(String original) { this.value original.value; this.hash original.hash; }在 JDK 9 及之后对应实现是public String(String original) { this.value original.value; this.coder original.coder; this.hash original.hash; }可以看到这个构造器没有复制底层数组而是直接把参数original的数组引用拿过来复用。也就是说通过new String(111)创建的新对象和字面量111对应的对象底层共享同一个数组。这正是很多候选人漏算数组对象的原因因为底层数组只有一个大家会下意识忽略它但从“创建了几个对象”的严格口径看这个被共享的数组确实是一个独立创建出来的数组对象。五、从字节码看 new String(111)理论分析最终要靠字节码验证。我们用 javap 反编译一段最简单的代码看看这行代码在 JVM 眼里到底是什么样子。5.1 源代码public class StringDemo { public static void main(String[] args) { String s new String(111); } }5.2 编译并查看字节码在命令行中执行javac StringDemo.java javap -c -verbose StringDemo截取与本题相关的反编译结果Constant pool: #1 Methodref #6.#19 // java/lang/Object.init:()V #2 String #20 // 111 #3 Class #21 // java/lang/String #4 Methodref #3.#19 // java/lang/String.init:(Ljava/lang/String;)V #5 Class #22 // StringDemo #6 Class #23 // java/lang/Object #20 Utf8 111 #21 Utf8 java/lang/String #22 Utf8 StringDemo #23 Utf8 java/lang/Object public static void main(java.lang.String[]); Code: 0: new #3 // class java/lang/String 3: dup 4: ldc #2 // String 111 6: invokespecial #4 // Method java/lang/String.init:(Ljava/lang/String;)V 9: astore_1 10: return5.3 四条关键指令main 方法里与对象创建直接相关的指令是new、dup、ldc和invokespecial我们逐一拆解。new 指令new指令会在堆上为 String 对象分配内存并把对象的引用压入操作数栈。注意此时对象还没有被构造字段还是默认值。也就是说new只负责“圈地”不负责“装修”。dup 指令因为构造方法被调用后操作数栈顶的对象引用会被消费掉而后续还要把这个引用保存到局部变量s所以需要dup复制一份引用。这也是 Java 对象创建过程中非常经典的 dup 用法。ldc 指令ldc负责把常量池中的常量加载到操作数栈。这里ldc #2加载的是 Class 常量池里的String 111项。当 JVM 首次执行这条 ldc 指令时会触发字符串字面量的解析JVM 先到 StringTable 中查找是否有内容等于111的 String 对象如果找到直接返回该对象引用如果没找到就在堆上创建一个新的 String 对象并把它的引用放入 StringTable然后返回。invokespecial 指令invokespecial #4调用的是 String 的构造器方法签名是init(Ljava/lang/String;)V也就是String(String original)。这个调用会把原先new创建的空壳对象和ldc返回的字面量引用分别作为对象和参数传入执行构造器内部的两行赋值。5.4 Class 常量池与字符串常量池的关系这里容易产生一个误会Class 常量池里的#2 String #20是不是就是字符串常量池里的对象答案是否定的。Class 常量池是 class 文件里的一份静态描述它记录的是“我用到过字符串 111”这一事实。字符串常量池是 JVM 运行期维护的全局缓存。两者通过 ldc 指令在运行期建立联系。换句话说只有真正执行到 ldcJVM 才会去 StringTable 查找或创建真正的 String 实例。这也是为什么同一个字符串字面量在类加载阶段就已经完成了静态解析准备而 ldc 执行时往往只是“命中缓存”而已。六、完整执行流程逐步拆解现在我们完整推演一遍String s new String(111)在 JDK 8 下、常池中没有111时的执行过程。6.1 步骤一字符串字面量被解析进字符串常量池在类加载阶段JVM 会解析 StringDemo 类的运行时常量池。遇到CONSTANT_String_info项时会尝试在 StringTable 中查找内容为111的字符串。由于这是第一次出现该字面量StringTable 中没有命中于是 JVM 会在堆上执行以下操作创建一个长度为 3 的char[]数组内容为{1,1,1}创建一个 String 对象让它的value字段指向这个 char 数组把这个 String 对象的引用放入 StringTable。到这一步堆上已经有两个对象一个 char 数组和一个表示字面量的 String 对象。6.2 步骤二new 指令创建新的空壳 String执行new #3时JVM 在堆上再分配一块内存生成第二个 String 对象。这个新对象此时还没有绑定任何 value 数组字段是默认值。6.3 步骤三构造器让新对象共享底层数组执行invokespecial调用String(String original)构造器时original就是步骤一里创建的字面量 String 对象。构造器执行this.value original.value; this.hash original.hash;于是第二个 String 对象的value字段也指向同一个 char 数组。没有创建新的 char 数组。最终s指向第二个 String 对象而字符串常量池保存着第一个 String 对象的引用。两个 String 对象底层共享同一个 char 数组。6.4 一图看懂引用关系文字描述容易绕晕我们用一张结构图来表示StringTable | | 引用 v String 对象 A ---- 字面量 111 对应的对象被字符串常量池引用 | | value 字段 v char[] value {1, 1, 1} ---- 唯一一个底层数组 ^ | value 字段 | String 对象 B ---- new 出来的对象局部变量 s 指向它这张图是本题的灵魂。如果你能在面试时把这张图讲出来基本就超越了大多数候选人。七、精确计数到底几个对象有了前面的基础现在可以给出精确答案。先说结论这道题没有唯一答案必须分版本、分场景、分口径。7.1 先明确统计口径在面试中通常所说的“几个对象”默认指 String 实例不统计 String 底层的 char 数组或 byte 数组。但如果你想展示深度可以主动补充如果把底层数组也算进去结论会更具体。7.2 场景一JDK 8 及以前常量池中没有 111这是最经典的场景也是“两个对象”说法的来源。字符串常量池没有111字面量解析时创建第 1 个 String 对象同时创建一个长度为 3 的 char 数组new指令创建第 2 个 String 对象构造器复用原 char 数组不再新建数组。按“只算 String 对象”的口径创建 2 个 String 对象。按“把数组也算入”的口径创建 3 个对象即 2 个 String 对象加 1 个 char 数组对象。7.3 场景二JDK 8 及以前常量池中已有 111如果在这行代码之前已经通过别的方式让 StringTable 中有了111例如先执行过String a 111;或调用过intern()那么执行new String(111)时ldc 命中 StringTable直接返回已有字面量 String 对象不再创建新的字面量 String也不再创建新的 char 数组new仍会创建 1 个新的 String 对象新对象复用已有底层数组。按只算 String 的口径只创建 1 个 String 对象。这就是“两个对象”说法最容易翻车的追问点。7.4 场景三JDK 9 及以后JDK 9 之后底层从char[]换成byte[] value coder。对字符串对象数量的结论没有影响常量池中没有111时仍会创建 2 个 String 对象底层数组则变成 1 个长度为 3 的 byte 数组。所以按严格口径JDK 9 及以后是 2 个 String 对象加 1 个 byte 数组对象共 3 个对象如果把数组排除在外仍是 2 个 String 对象。7.5 场景四底层数组是否算入如果把“对象”定义为 JVM 堆上所有通过 new、数组创建等方式产生的实体那么在最经典的 JDK 8 场景下答案是 3 个字面量对应的 String 对象new 创建的 String 对象两个 String 共享的 char 数组对象。不过在实际面试中面试官通常默认只关心 String 对象。因此回答时可以这样说“如果只算 String 对象是 2 个如果把底层 char 数组也算进去是 3 个。而且如果常量池里已经有这个字符串就只会新建 1 个 String 对象。”这样既给了标准答案又展示了全面性。7.6 汇总表场景JDK 版本常量池状态String 对象数底层数组对象严格口径总对象数经典场景JDK 8 及以前没有 11121 个 char[]3字面量已存在JDK 8 及以前已有 11110 个新数组1Compact StringsJDK 9 及以后没有 11121 个 byte[]3Compact Strings 且已存在JDK 9 及以后已有 11110 个新数组1这张表可以作为面试时的快速参考但更重要的是理解每一行背后的执行逻辑否则换个字符串内容、换个写法就可能答错。八、用工具亲自验证理论分析最终要靠工具验证。下面介绍三种验证方式分别从字节码、对象内存布局、堆内对象三个角度交叉确认。8.1 javap 查看常量池与指令前面已经用javap -c -verbose看到了关键指令。这里强调一下new和ldc的顺序先new分配新对象再ldc加载字面量最后invokespecial执行构造器共享数组。如果你执行两次new String(111)字节码中ldc #2仍然只会对应一个常量池项说明两行代码共享同一个字面量入口。这从字节码层面印证了字面量复用机制。8.2 JOL 观察对象布局JOL 是 OpenJDK 提供的对象布局分析工具。通过它可以看到 String 对象内部字段以及底层数组占用的空间。import org.openjdk.jol.info.GraphLayout; import org.openjdk.jol.vm.VM; public class JolDemo { public static void main(String[] args) { System.out.println(VM.current().details()); String a 111; String b new String(111); System.out.println(GraphLayout.parseInstance(a).toFootprint()); System.out.println(GraphLayout.parseInstance(b).toFootprint()); } }运行后可以观察到a和b是两个不同的 String 对象实例它们各自有一个引用字段在 JDK 9 以上是byte[] value两者底层数组的地址是同一个证明构造器确实复用了数组对象。8.3 JHSDB 或 OQL 查看堆内对象还可以通过堆转储文件加上 Eclipse Memory Analyzer 的 OQL 查询来验证。先给程序加上堆转储参数运行-XX:HeapDumpOnOutOfMemoryError或者主动调用 HotSpotDiagnosticMXBean 生成 heap dump。拿到 heap dump 后在 MAT 中执行类似这样的 OQLselect s from java.lang.String s where s.toString() 111你会看到至少两个 String 实例其中一个被 StringTable 引用另一个被局部变量引用。再进一步查看它们内部的 value 数组地址可以发现地址相同。这些工具方法并不是面试必考点但如果你能主动说出“我实际用 JOL 验证过”会极大增强回答的说服力。九、深入 String.intern()这道题经常和intern()放在一起考因为两者都围绕字符串常量池展开。理解了 intern才算真正理解了字符串常量池的运行机制。9.1 intern 的契约String.intern()的作用是如果字符串常量池中已经存在内容等于当前字符串的对象就返回池中的那个对象否则把当前字符串对象加入字符串常量池并返回它。注意从 JDK 7 开始池中保存的是对象引用所以“加入池中”本质上就是往 StringTable 里放入一个引用。9.2 JDK 6 与 JDK 7 之后的行为差异JDK 6 时字符串常量池在永久代。当调用intern()且池中不存在该字符串时JVM 会在永久代中复制一份新的字符串对象放入池中。堆上的原对象和池中的对象并不是同一个实例。JDK 7 之后字符串常量池在堆中。当调用intern()且池中不存在该字符串时JVM 不再复制而是直接把当前堆中 String 对象的引用放入 StringTable。因此可能出题String s new String(111); String s2 s.intern(); String s3 111; System.out.println(s s2); // false System.out.println(s2 s3); // true结果分析如下new String(111)创建了一个新的堆对象s而字符串常量池中已经有字面量111对应的对象s.intern()发现池中已有111于是返回池中的那个字面量对象所以s2是字面量对象本身s3 111同样从池中拿到字面量对象因此s s2为 falses2 s3为 true。如果把代码改成下面的形式JDK 7 前后答案又会不同String s new StringBuilder().append(1).append(11).toString(); String s2 s.intern(); String s3 111; System.out.println(s s2); System.out.println(s2 s3);这里111字面量从未直接作为字面量出现在源码里所以字符串常量池一开始没有111。s.intern()会把s的引用放入池中。于是s s2和s2 s3在 JDK 7 之后都为 true在 JDK 6 中因为发生复制结果则是 false。这类题是intern()的经典跨版本陷阱。9.3 为什么本题会引申到 intern因为new String(111)的关键点就是“字面量进池”与“new 出来的对象不进池”之间的矛盾。理解 intern 能帮我们明确字面量天然和常量池绑定new 出来的对象默认不在池中除非显式调用 internJDK 7 之前和之后intern 对对象身份的保留策略不同。十、美团面试高分回答示范面试不是背答案而是展示结构化思维。同一个问题不同深度的回答会带来完全不同的评价。10.1 及格答案“如果没有特殊上下文我认为会创建两个 String 对象。一个是字符串常量池里的111一个是 new 出来的 String 对象。如果常量池里已经有111那就只创建一个对象。”这个回答能说清主结论也能回应最常见的追问属于及格水平。10.2 进阶答案在及格答案基础上补充三点指出前提“两个对象”默认讨论的是 String 对象而不是底层数组说明 String 构造器复用底层数组指出 JDK 版本差异JDK 8 及以前底层是 char 数组JDK 9 之后是 byte 数组加 coder。10.3 满分答案的完整逻辑一个比较有说服力的完整回答可以是这样的这道题不能简单说两个对象要先明确场景。假设当前字符串常量池里还没有111以 JDK 8 为例类加载解析字面量时JVM 会在堆上创建一个 String 对象并创建一个长度为 3 的 char 数组然后把该 String 引用放入字符串常量池。执行 new 指令时JVM 又在堆上创建一个新的 String 对象。构造器 String(String original) 没有复制数组而是直接让新对象的 value 字段复用原数组。所以如果只算 String 对象是 2 个如果把底层 char 数组也算对象就是 3 个。如果常量池里已经有111ldc 直接命中那么只会新建 1 个 String 对象。JDK 9 之后逻辑类似只是底层变成了 byte 数组加 coder。如果面试官愿意继续展开我还可以从字节码角度看这个问题main 方法里对应的是 new、dup、ldc、invokespecial 四条关键指令。这样的回答既给出了标准答案又展示了条件意识、源码意识、版本意识和字节码意识属于比较完整的高分回答。十一、变体题把这道题彻底吃透美团等公司很少单独问这一题更多时候会连续追问。下面把最常见的几种变体一起梳理一遍。11.1 String s 111 创建几个对象如果常量池中没有111执行String s 111会创建一个 String 对象并同步创建一个底层 char 数组或 byte 数组。只算 String 对象就是 1 个。如果常量池已有则不再创建新对象直接返回池中引用。对比new String(111)多出来的就是那个显式 new 出来的实例。11.2 String s 1 11 创建几个对象public class ConstantFoldDemo { public static void main(String[] args) { String s 1 11; String t 111; System.out.println(s t); // true } }由于1和11都是编译期常量编译器会做常量折叠把1 11直接优化成111。因此运行期只会涉及字面量111一个字符串创建 1 个 String 对象。程序输出 true。11.3 new String(1) new String(11) 创建几个对象public class NewPlusNewDemo { public static void main(String[] args) { String s new String(1) new String(11); } }由于参与拼接的不是编译期常量而是两个 new 出来的变量编译器不会在编译期折叠而是会在运行期使用 StringBuilder 完成拼接。大致计数如下new String(1)若常量池没有1创建 1 个字面量 String 加 1 个 new 出来的 String再算上底层数组对象更多new String(11)同理拼接过程创建 1 个 StringBuilder 对象StringBuilder.toString() 内部会 new 一个 String 对象表示拼接结果111。这道变体题重点不是让你数出具体总数而是考察你是否知道编译期常量拼接会被折叠而运行时变量拼接会退化为 StringBuilder。两种写法虽然结果相同内存行为完全不同。11.4 与 equals 的经典坑public class EqualsDemo { public static void main(String[] args) { String a 111; String b new String(111); System.out.println(a b); // false System.out.println(a.equals(b)); // true } }比较引用是否指向同一个对象equals比较字符串内容是否相同。因为a来自常量池b是 new 出来的新对象所以引用不同结果为 false但内容都是 111所以 equals 为 true。11.5 intern 连环题比较常见的连环问法是String s1 new String(111); String s2 111; String s3 s1.intern(); System.out.println(s1 s2); // false System.out.println(s2 s3); // true因为111字面量已经在池中所以s1.intern()返回池中原有对象s1本身仍是 new 出来的那个对象因此s1 s2为 false。再换一种写法String s1 new String(1) new String(11); s1.intern(); String s2 111; System.out.println(s1 s2);如果字符串常量池之前没有111JDK 7 之后s1.intern()会把s1的引用放入池中s2 111会命中该引用因此输出 true。JDK 6 中因为 intern 会复制对象结果则为 false。这道题的版本差异非常经典。11.6 合成大西瓜多行代码综合计数把前面知识点组合成一道综合题String s1 111; String s2 111; String s3 new String(111); String s4 s3.intern(); System.out.println(s1 s2); // true System.out.println(s1 s3); // false System.out.println(s1 s4); // true分析s1创建字面量并放入池中s2命中池中同一对象s3通过 new 创建一个新对象s3.intern()返回池中已有对象即s1指向的对象所以s1 s2trues1 s3falses1 s4true。如果能把每一行的对象创建和引用关系讲清楚这道题才算是真正吃透了。十二、常见误区清单下面把初学者和面试候选人最容易犯的错误集中列出方便你自查。误区一把单引号当成字符串字面量Java 中单引号是 char 类型双引号才是 String 类型。111是非法写法new String(111)无法编译。这是本题第一道门槛。误区二认为字符串常量池里存的是字符内容字符串常量池里保存的是 String 对象的引用不是字符序列本体。真正存储字符的数组在堆上。误区三认为构造器会复制一份底层数组String(String original)构造器直接复用原数组不会新建数组。这也是为什么两个 String 会共享底层数组。误区四把数组对象和 String 对象混为一谈说“两个对象”时一般是指两个 String 对象。若把 char 数组或 byte 数组也算进去经典场景是三个对象。误区五忽略常量池已有字符串的情况如果池中已有111new String(111)只新建一个 String 对象字面量不会再新建。误区六认为 new 出来的对象会自动进入字符串常量池new 出来的对象默认不在池中除非显式调用intern()。字面量才天然和常量池绑定。误区七混淆编译期常量折叠与运行期拼接1 11是编译期常量折叠结果为字面量111new String(1) new String(11)是运行期 StringBuilder 拼接内存行为完全不同。误区八不区分 JDK 版本JDK 6 字符串常量池在永久代JDK 7 后移到堆JDK 9 后 String 底层从 char 数组改为 byte 数组。回答这类题时要说明版本前提避免跨版本翻车。十三、总结回到最开始的问题美团面试题String s new String(111)会创建几个对象最稳妥的回答不是直接报一个数字而是这样组织先纠正可能的单引号笔误确认题目是双引号版本说明“两个对象”是指两个 String 对象前提是常量池中此前没有111解释第一个 String 来自字符串字面量解析由 StringTable 引用第二个 String 来自 new 指令补充 String 构造器复用底层数组因此如果数组也算对象JDK 8 下是三个对象如果常量池已有111则只新建一个 String 对象如果讨论 JDK 9 以上底层数组是 byte 数组加 coder但 String 对象数量结论不变。总结成一句话默认场景下是 2 个 String 对象严格口径下是 2 个 String 加 1 个底层数组对象常量池命中时则只有 1 个 String 对象同时要注意 JDK 版本和统计口径。这道题表面考的是对象数量实际上考的是你对字符串常量池、String 底层实现、JVM 字节码和 JDK 版本演进的整体理解。把这些链路打通你在面对任何字符串相关面试题时都会更有底气。十四、延伸阅读JVM 规范运行时常量池与字符串字面量加载JEP 254: Compact StringsJava 8 String 官方文档javap 反编译工具官方说明OpenJDK JOL 对象布局分析工具
