ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java面试必问:new String(“abc“)到底创建了几个对象?

Java面试必问:new String(“abc“)到底创建了几个对象? 开局先说结论这道题在 Java 面试里出现的频率极高甚至很多五六年经验的人也未必能答得滴水不漏。看到String str new String(abc)时面试官真正想考察的不是“会不会背答案”而是你对 JVM 内存结构、字符串常量池、对象创建过程以及 JDK 版本差异的理解深度。如果你只想甩一句“两个对象”那大概率会被继续追问一直问到怀疑人生。这篇文章就把这个问题彻底拆开从原理到源码从字节码到实际开发中的避坑建议一次性讲透。1. 问题背后的考察点面试官到底想问什么1.1 从一行代码看Java内存模型String str new String(abc)拆开来看包含了两层语法动作左边声明了一个String类型的引用变量str右边先用new关键字创建了一个String对象然后又用字符串字面量abc作为构造参数传入。很多人会把“引用”和“对象”混为一谈。其实str本身只是一个栈内存中的引用真正存储字符串内容的对象在堆内存里。面试官问“创建了几个对象”问的是对象实体不是引用。普通情况下这一行代码可能会创建两个对象一个是在字符串常量池中的字面量abc一个是堆中通过new创建出来的新String对象。但还有一种可能如果常量池中已经存在abc那么new String(abc)就只会在堆上创建一个新对象常量池中的对象是复用的。这里有个很多人忽略的细节字符串常量池里的对象在 JDK 7 之前放在方法区永久代JDK 7 及之后移到了堆中。位置变了但“池中存的是 String 对象”这一点没变。也就是说字面量abc在类加载或首次被引用时会在常量池中生成一个 String 对象new则在堆中复制一个内容相同、但身份全新的对象。1.2 常问变体与面试官的层层深挖这道题的变体非常多面试官会根据你的回答不断加深难度。比如String s abc创建了几个对象会复用常量池中已有的对象String s new String(abc)再加上.intern()又是什么情况如果常量池已有则返回池中的引用堆中的对象可以被 GC 回收循环中拼接字符串会创建多少个对象这涉及到StringBuilder的优化null abc会怎样等等。我见过最狠的面试官会让候选人手写一个new String(new char[]{a,b,c})和new String(abc)的区别。其实前者不会把字符串放入常量池除非手动intern()因为它传入的是字符数组不会触发字面量入池。这些变体本质上都在考同一个点你是否清楚字符串对象的创建时机和存储位置。2. 深度解析new String(abc) 在JDK各版本中的行为差异2.1 经典答案“一个还是两个”的逻辑先把最标准的书面答案写出来如果之前从没出现过字符串字面量abc那么String str new String(abc)会创建两个对象一个在字符串常量池中abc一个在堆内存中new String(...)。如果常量池中已经存在abc例如前面代码里写过String s abc那么这行代码只会创建一个堆对象池中的abc是复用已有的。注意这两个答案都默认“创建对象”不包括 JVM 内部的char[]数组。因为String在 Java 9 之前内部是char[]Java 9 以后改成了byte[]new String(abc)构造时还会复制数组内容。但面试时一般不会把数组算作单独的“对象”否则没法聊了。我建议面试时先给出上面的标准答案然后主动补充一句“这是不考虑常量池是否已有该字符串的前提我可以从字节码层面再展开。”这样能体现出你的严谨也把话语权掌握在自己手里。2.2 JDK 7前后字符串常量池的位置变化为什么要把 JDK 版本单独拎出来因为 JDK 7 对字符串常量池做了一次大挪移影响了对“对象在哪块区域”的回答。JDK 6 及之前常量池在永久代PermGennew String(abc)创建的两个对象不在同一个区域一个在永久代一个在堆JDK 7 起常量池移到了堆中。从“物理位置”上看常量池中的abc和堆上的new String对象都在堆里但常量池有自己的逻辑管理区域不会因为是同一个区域就变成同一个对象。这个位置变化带来的另一个后果是String.intern()的行为发生了变化。JDK 6 中intern()是把对象复制到永久代并返回永久代引用JDK 7 中intern()如果发现池中没有会把堆中该对象的引用记录到常量池返回的是同一个堆对象的引用。因此下面这段代码在 JDK 6 和 7 上结果不同String s1 new String(a) new String(b); s1.intern(); String s2 ab; System.out.println(s1 s2);JDK 6 下是falseJDK 7 下是true。原理就在于ab原本不在池中intern()在 JDK 7 里记录的是s1的引用后面ab字面量直接复用了这个引用。2.3 通过字节码验证实际创建流程与其口头争论不如用字节码说话。写下面这段代码编译后用javap -c查看public class StringCreate { public static void main(String[] args) { String str new String(abc); } }对应字节码关键部分如下public static void main(java.lang.String[]); Code: 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String abc 6: invokespecial #4 // Method java/lang/String.init:(Ljava/lang/String;)V 9: astore_1 10: returnldc指令会负责把abc这个符号引用解析为常量池中的 String 对象如果常量池中还没有就会去创建new指令创建堆中的 String 实例invokespecial调用构造器构造器内部实际上又把abc内部字符数组复制了一份。所以从 JVM 指令层面看创建过程中包含了“池中字面量”和“堆中的新 String 实例”两层动作。如果不想踩坑可以做个实验连续两次执行String s new String(abc)然后打印System.identityHashCode或者用比较你会发现两次new出来的对象肯定不同但它们的equals相同。这说明池中的字面量只创建了一个堆里的副本却造了两个。3. 从源码与JVM角度理解字符串对象创建3.1 String构造器源码分析JDK 8 的String(String original)构造器长这样public String(String original) { this.value original.value; this.hash original.hash; }很多人以为这里只是“引用赋值”没有复制数组所以会认为new String(abc)其实没在堆上复制数据只是包装了一下。这种理解不完全对。注意这是 JDK 8 的源码虽然value数组引用被直接赋值了看起来共用同一份char[]但new出来的String对象本身是新的它只是在内部指向了同一个char[]。也就是说对象是新建了字符数据没有复制。到了 JDK 9 之后String 改为byte[]构造器变成public String(String original) { this(original.toCharArray()); }它会先把原字符串转成字节数组再拷贝一份这意味着新的String对象内部数组也是独立的。所以不同 JDK 版本下“复制”的粒度也不一样。面试时可以补一句“从 JDK 9 开始构造时内部数组会重新复制一份从内存开销上更重。”3.2 intern()方法的作用与字符串池缓存机制intern()是回答这道题绕不开的关键点。它的文档说明是如果常量池中存在与当前字符串equals的字符串则返回池中的字符串否则把当前字符串加入池中并返回其引用。在 JDK 7 之后加入池中的其实是对堆对象的引用而不是复制品。这里有个经典陷阱String s1 new String(abc); String s2 s1.intern(); System.out.println(s1 s2); // false String s3 abc; System.out.println(s2 s3); // true因为s1构造时池里已经有abc了所以intern()直接返回池中的abc它和s1根本不是同一个对象。只有当你用new String(a) new String(b)这种不会自动入池的方式拼出ab后再intern()才有可能让堆引用和池引用合一。日常开发中我并不推荐滥用intern()。因为常量池一旦膨胀走的是哈希结构冲突多了反而影响性能而且intern()本身有 JNI 成本。只有当你确定同一字符串会被大量重复使用并且数量有限时才值得考虑用池化来省内存。3.3 Compact Strings与JDK 9后的底层存储JDK 9 引入了Compact StringsJEP 254底层存储从char[]改成了byte[]并且用一个coder字段标记编码格式LATIN1ISO-8859-1如果字符串所有字符都在单字节范围内就用单字节存省一半空间UTF16只要有字符超出单字节范围就会退回到双字节。这个改动对对象创建数量的影响不大但对内存占用影响很大。很多面试官会顺带问“那new String(abc)创建的String对象内部数组有多大”如果你能答出“在 JDK 9 后abc是 LATIN1 编码内部byte[]长度是 3再加对象头、属性等实际占用比 JDK 8 更小”他们会觉得你对 JVM 底层确实有研究。4. 面试延伸String、StringBuilder、StringBuffer与对象创建4.1 为什么说字符串拼接会创建大量对象经典问题先上代码String s ; for (int i 0; i 10; i) { s i; }很多人以为这只是在s后面追加数字实际上每次s i都会在循环体内新建一个StringBuilder调用append再toString()生成新的String并赋值给s。这一步会创建至少两个对象一个StringBuilder一个String。循环 10 次就有 20 个左右对象如果循环 1000 次垃圾回收压力会非常明显。反编译能看到编译器确实会把s i优化成s new StringBuilder().append(s).append(i).toString();注意变量s的引用被不断替换原来的字符串对象变成可回收垃圾。所以面试题如果问“拼接优化”最佳答案不是“用StringBuilder还是”而是“在循环内部请手动创建StringBuilder避免每次循环都新建对象”。这也解释了为什么现在比较新的 JDK 里单个直接使用做少量拼接是可以的编译器会帮你建StringBuilder但循环里编译器没法聪明到复用同一个 builder。4.2 StringBuilder与StringBuffer的选择StringBuilder和StringBuffer都是可变字符序列底层都是char[]JDK 9 后同样有 compact 相关实现。两者最大的区别是StringBuffer的方法加了synchronized保证线程安全StringBuilder没加性能更高。但在老版本 HotSpot 中StringBuffer因为toStringCache机制某些场景下可能不比StringBuilder慢太多而在现代 JDK 中官方也建议优先使用StringBuilder。面试中如果你提到StringBuffer最好能补充一个冷知识StringBuffer有个insert、append方法返回this支持链式调用但它转换为String是通过toString()方法而从String转到StringBuffer最常用的方式是构造器传入StringBuffer sb new StringBuffer(str);StringBuilder同理。如果你需要线程安全又不希望直接加锁可以考虑ThreadLocalStringBuilder或者String.concat等方案不过这些是进阶话题。4.3 String转StringBuffer的常见方式热搜词里多次出现“stringbuffer转换为string”这确实是面试常出的细节题。核心大概分两类StringBuffer转StringStringBuffer sb new StringBuffer(hello); String str sb.toString();或者用new String(sb)但String构造器不支持直接传StringBuffer实际上有String(StringBuffer)构造器但已标记为 deprecated更推荐sb.toString()。String转StringBufferString str hello; StringBuffer sb new StringBuffer(str);不要用StringBuffer.append(str)之后再toString()除非你本来就是想链式操作。强调一个坑StringBuffer和String的转换不会修改原对象两者内容相同但身份不同。如果你在转换后修改StringBuffer原来的String不会变因为它本质是不可变对象。5. 实操总结如何回答与代码避坑指南5.1 面试时的完整回答模板如果你希望在一分钟之内给面试官留下好印象可以这样组织回答先分情况如果常量池中没有abc那么代码会创建两个对象如果已经有则只创建一个堆对象。指出为什么字面量abc会触发字符串常量池的创建new会在堆中创建一个新实例。主动补一句JDK 7 之后常量池在堆中JDK 9 之后底层数组从char[]变成byte[]但对象创建逻辑不变。再补一个例子如果调用intern()返回的可能是已有池对象而不是new出来的对象。这样答案的深度和覆盖率都足够。面试官如果继续追问大概率会顺着“常量池在哪”“生产上怎么避免创建多余对象”往下走你也能自然应对。5.2 日常开发中的字符串对象优化建议工作中不能只知道理论给你几条我踩过坑后总结出来的建议能用字面量不用 newString s abc比String s new String(abc)少创建一个堆对象而且常量池还能复用。循环拼接字符串一定复用 StringBuilder别让编译器在一遍遍循环里重复创建 builder。确认字符串是否真的需要 intern大量低重复率的字符串调用intern()会让常量池成为大杂烩GC 和查表都会有压力。返回字符串时避免无意义的包装比如new String(result.getBytes(...))这种代码大多是多此一举还可能因为字符集不一致产生乱码。第三方接口返回的字符串别急着 new 新对象直接使用引用即可除非你需要基于它生成不同内容的副本。还有一点String是不可变的线程安全是它的天然优势。在并发场景下用String作为缓存 key或者作为不可变对象共享这一点反而是加分项。5.3 常见问题速查表为了让你临阵磨枪把这道题和相关扩展做成速查表问题答案String s abc创建了几个对象如果池中没有则 1 个池中对象有则 0 个复用。new String(abc)创建了几个对象通常 1 或 2 个取决于池中是否已有abc。new String(abc).intern()的返回值能直接赋值给s可以返回的是池中对象堆中的new对象会被回收。String s a b c创建了几个对象编译期优化成abc一般 1 个池对象。循环里s i有什么问题每轮创建新StringBuilder和新的StringGC 压力大。StringBuilder和StringBuffer选哪个无并发用StringBuilder有并发且想省事用StringBuffer但也可以考虑自建锁逻辑。StringBuffer怎么转String调用toString()即可。String怎么转StringBuffernew StringBuffer(str)或new StringBuilder(str)。这里再送你一个独家小技巧如果你在 JDK 8 下做字符串池大小调优可以用-XX:StringTableSize参数控制哈希桶数量默认值是 60013JDK 8 版本不同可能不一样。遇到大量intern()导致性能下降时调大这个值有时能立竿见影。但 JDK 9 之后某些版本默认值已经改了先用java -XX:PrintFlagsFinal -version | grep StringTableSize看看实际配置再动手。最后再说点心里话我在实际面试别人或者帮朋友梳理这道题时见过太多“背答案”式回答。他们会说“两个对象”但你追问“为什么有时候是一个”就卡壳了。其实这道题最有趣的地方在于答案取决于“上下文”。就像问一个人现在是不是成年你要知道他出生日期一样。面试官真正想知道的是你会不会在写代码时意识到对象的产生成本以及你有没有深入了解一下眼前的这个String到底是什么样的结构。如果你下次再遇到这个问题我建议别急着背结论先反问一句“你指的是当前 JVM 版本下常量池还没有abc的情况吗”这一句反问反而会让人觉得你是个严谨的工程师。毕竟好的答案不只是对错而是建立在清晰的假设之上。
返回列表