创建了几个对象?深度解析字符串内存机制)
1. 这道题为什么是Java面试的“钉子户”先给结论通常来说是两个对象但在特定条件下可以是一个甚至在某些极端写法下会出现三个。说实话我第一次被问到这个问题的时候也愣了一下。当时刚工作没两年觉得自己Java基础还凑合结果面试官问“String str new String(abc)创建了几个对象”我脱口而出“两个”然后他追问“哪两个分别在哪儿”我才发现自己其实没有真正理解背后的机制只是背过答案而已。后来自己做了几年Java开发又面试过不少人发现这道题几乎是Java面试里出镜率最高的字符串问题之一。它看起来就一行代码但背后牵扯的东西太多了字符串常量池、堆内存、对象引用、编译期优化、类加载时机……随便往深了挖都能挖出好几个知识点。更重要的是这道题没有唯一标准答案。你说两个在绝大多数场景下是对的但如果你能说明白“什么情况下是一个”说明你对JVM内存模型的理解到位了。反过来如果你只会背“两个对象”面试官大概率会继续追问这两个对象分别在内存的哪个区域“abc”这个字符串本身是不是对象常量池里的对象和堆里的对象内容一样吗如果字符串是变量拼接呢如果是final修饰呢所以我一直觉得这道题考察的其实不是“你会不会背答案”而是你有没有真的理解Java字符串的底层设计。这篇就结合我自己的理解把这道题从头到尾拆一遍包括对象个数、内存分布、字节码层面的证明以及面试官后续可能追问的一系列变种问题。先说清楚一个基础概念后面所有讨论都建立在这上面Java里的字符串对象分为两种存在形式——常量池中的对象和堆中的对象。这两个概念不搞清楚这道题怎么答都是虚的。2. 先拆解这行代码到底干了什么2.1 “abc”字面量编译期就进常量池我们平时写String s abc这里的abc是字符串字面量String Literal。Java编译器和JVM对字面量有特殊处理在编译阶段编译器会把代码中用到的字符串字面量收集起来放到class文件的常量池Constant Pool里类加载的时候JVM会把这些字符串字面量加载到运行时常量池并进一步放入字符串常量池String Pool。这个字符串常量池在JDK 7之前位于方法区PermGenJDK 7开始移到了堆内存中JDK 8以后方法区换成了元空间Metaspace但字符串常量池仍然在堆里。这个细节后面讲内存分布的时候还会用到。关键点是字符串字面量本身就是对象。abc在JVM看来就是一个java.lang.String实例只不过它被特殊管理在常量池里。所以当你写String str new String(abc)的时候光这行代码里出现的那对引号abc就会触发JVM去常量池里找——如果常量池里还没有这个字符串就创建一个如果已经有了就直接复用。提示这里的“创建一个”发生在类加载阶段还是执行阶段其实有争议。严谨的说法是类加载过程中的解析阶段会去常量池里查找并创建对应的字符串实例。但对于面试回答来说你只需要说明“执行这行代码时常量池里如果没有abc这个字符串会先创建一个”就够了。2.2 new String(abc)显式在堆里new一个对象new关键字的作用很直接在堆内存中分配一块空间创建一个新的String对象。这个对象的内容是传入的参数字符串“abc”但它是独立于常量池对象的一个新对象。注意new String(abc)的构造过程并不是“复制”了常量池里的对象而是用常量池里那个字符串的内容作为初始值在堆上创建一个全新的String实例。两个对象内容一样但引用不同身份不同。于是这行代码在“常量池原本没有abc”的前提下一共创建了两个对象常量池中的 “abc” 字符串对象——由字符串字面量触发创建。堆中的 new String(abc) 对象——由new关键字显式创建。然后str这个引用变量指向的是堆里的那个新对象而不是常量池里的对象。用个不恰当的类比常量池里的“abc”就像是一个公开的模板文件new String(abc)就是基于这个模板在堆里复制了一份全新的文件。两份文件内容一模一样但一个是原件、一个是副本各自独立存放。2.3 那个“一个对象”的例外情况到底怎么回事面试官问“创建了几个对象”如果你的回答只有“两个”其实是不够的。因为存在一种场景这行代码执行的时候常量池里已经有了“abc”这个字符串。比如代码前面已经写过String s1 abc; String str new String(abc);在这种情况下第一行执行时常量池里没有“abc”于是创建了一个常量池对象。第二行执行时常量池里已经有“abc”了new String(abc)只会额外在堆里创建一个对象。所以对于这一行代码本身来说它只创建了一个对象堆里的那个因为常量池里的对象不是这一行创建的。这就是为什么我说这道题没有绝对答案——它取决于“abc”这个字符串常量在你的程序运行过程中是否已经被加载到常量池。绝大多数情况下new String(abc)是程序里第一次出现这个字面量所以是两个对象。但如果上下文里已经出现过就是一个对象。注意还有一种情况需要区分——类是同一个类还是不同类。字符串常量池是JVM全局共享的不是类级别隔离的所以即使“abc”是在别的类里先出现的只要常量池里有了再次执行new String(abc)就只创建一个堆对象。2.4 常见的错误答案和误区我在面试别人的时候经常听到以下几种回答第一个误区认为创建了三个对象。理由是“abc”一个、new String一个、引用str一个。这明显是把“对象”和“引用”搞混了。str只是一个局部变量存在栈帧的局部变量表里它本身不是对象只是一根“绳子”用来牵住堆里的对象。扩展一下引用变量在栈中占4个字节32位系统或8个字节64位系统取决于JVM配置但它绝不是一个String对象。第二个误区认为只创建一个对象。有人会拿“常量池里已经存在abc所以只new了一个”来回答但没注意到前提条件。如果面试官问的是“这行代码创建了几个对象”而不给任何上下文默认情况下我们认为是首次执行也就是两个。第三个误区搞不清new String(abc)和abc的区别。前者永远在堆里创建一个新对象后者只在常量池里没有时才会创建。前者返回堆对象引用后者返回常量池对象引用。两者用比较永远不等除非有特殊优化但Java规范里好像没有这个优化实际也不存在。顺着这个思路可以再看一个清单把这行代码相关的对象、区域、触发条件理清楚对象或引用所在区域创建时机创建条件常量池中的“abc”堆中的字符串常量池类加载/执行阶段常量池中不存在该字符串时堆中的 String 实例Java堆执行new指令时每次执行都会创建str 引用变量栈帧局部变量表执行方法时每次执行都会分配但不是对象这张表基本就是这道题的核心脉络。下面我会从字节码角度把这行代码的执行过程再往下挖一层。3. 从字节码角度验证对象到底在哪创建3.1 javap反编译的结果分析纸上谈兵没意思我们把代码真正跑一遍。写个最简单的类public class StringTest { public static void main(String[] args) { String str new String(abc); } }编译之后用javap -c StringTest查看字节码main方法的字节码大概是这样的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: return来逐行分析这几条指令0: new在堆上分配一块内存创建了一个新的String对象但此时还没调用构造方法对象处于“半初始化”状态。这个对象就是我们在堆里创建的那个。3: dup复制栈顶的引用。因为接下来调用构造方法会把原来的引用消耗掉所以先复制一份后面astore_1还需要用到原始引用。4: ldc #3从常量池加载字符串“abc”。这一步才是触发常量池字符串创建的关键指令。如果常量池里还没有“abc”这一步会先在常量池里创建它如果已经有了就直接引用。6: invokespecial调用String的构造方法用常量池里的“abc”作为参数初始化堆里那个新对象。9: astore_1把堆对象的引用保存到局部变量表下标为1的位置也就是str。字节码很清楚地告诉我们new指令创建一个堆对象ldc指令负责加载必要时创建常量池字符串。这刚好对应了“两个对象”的回答。3.2 如果是String s abc呢作为对比再看一眼最简单的字符串赋值public class StringTest2 { public static void main(String[] args) { String s abc; } }对应字节码只有0: ldc #2 // String abc 3: astore_1 4: return没有new指令只有ldc。所以这种情况下最多创建一个常量池对象而且如果之前代码已经加载过“abc”连这一个都不会新建只是复用。这就是为什么我一直跟团队里的小朋友强调能用字面量就别用new String。不仅仅是为了少创建对象更重要的是字面量可以复用常量池里的实例而new String无论如何都会在堆里搞一个新的白白浪费内存。3.3 intern()方法在面试中怎么答聊到常量池面试官十有八九会顺带问intern()方法。这题和原题高度相关因为intern()就是连接堆对象和常量池对象的桥梁。简单说调用s.intern()JVM会检查字符串常量池里有没有内容与s相等的字符串。如果有直接返回常量池对象的引用。如果没有在JDK 7及以后版本中将当前堆对象的引用复制到常量池中实际上是记录引用然后返回这个引用。注意JDK 7前后的一个重要区别在JDK 6及之前字符串常量池在方法区PermGenintern()如果发现池里没有会把字符串对象复制一份放进方法区的常量池返回的是复制后的对象引用。JDK 7开始常量池移到了堆里intern()就不再做复制而是直接把堆对象的引用记录到常量池中。所以经典题来了String s new String(abc); String s1 s.intern(); System.out.println(s s1); // 输出什么答案是false。因为new String(abc)创建堆对象时常量池里已经有“abc”了构造参数里的字面量触发了创建所以s.intern()直接返回常量池里已有的那个“abc”对象跟堆里的s不是同一个引用。换一个极端例子String s new StringBuilder(ja).append(va).toString(); String s1 s.intern(); System.out.println(s s1); // 输出什么这个题在JDK 7及以上版本输出true因为在调用intern()之前“java”这个字符串没有作为字面量出现在代码里常量池中不存在所以intern()直接把堆上的引用记录进了常量池返回的也是堆对象本身的引用。但这事儿在Java后续版本里因为“java”字符串本身可能被JVM内部提前加载结果不稳定面试里一般不推荐举这个例子容易被带偏。回到主线的new String(abc)我建议你面试时这样回答最稳妥在一般情况下创建两个对象一个是常量池中的“abc”字符串对象一个是堆中的String对象。但如果在此之前“abc”已经存在于常量池中则只创建一个堆对象。严格来说创建几个对象取决于这行代码执行时常量池的状态。这个回答既给结论又给了条件显得你不仅知道答案还理解背后的机制。后面如果再追问细节就有充分的空间展开。4. 从JDK版本演进看字符串常量池的变迁4.1 JDK 6、JDK 7/8、JDK 9 这几个阶段的差异这道题还有一个隐藏考点字符串常量池在不同JDK版本中的位置和管理方式不同而“创建了几个对象”这个问题的某些细节会受此影响。先整理一下演进历程JDK版本字符串常量池位置特点JDK 6及之前方法区PermGen永久代常量池大小受限intern()使用不当容易OOM: PermGenJDK 7堆内存常量池从方法区移到堆intern()改为记录引用JDK 8堆内存方法区变为元空间但常量池在堆与JDK 7类似但PermGen被Metaspace替代不需要再调PermGen大小JDK 9堆内存字符串底层从char[]改为byte[]但常量池机制基本延续为什么JDK 7要做这个迁移根本原因是PermGen空间太小默认只有几十MB到一百多MB而字符串常量池里的内容是常驻的如果程序里用大量intern()或者动态生成大量字符串PermGen很容易被打满触发OutOfMemoryError: PermGen space。移到堆里之后字符串常量池可以享受堆内存的弹性管理不再受PermGen固定大小的限制GC也能正常回收常量池中不再被引用的字符串。JDK 9开始字符串内部表示从char[]变成了byte[]同时引入一个编码标志位。如果一个字符串的内容是Latin-1可表示的大部分英文字符串都是每个字符只占1个字节内存占用直接减半。这个优化对常量池里的海量字符串效果非常明显。但注意这个变化不影响“创建了几个对象”的答案它只是内存占用层面的优化。4.2 不同JDK版本下用一个例子验证结果的一致性说了这么多还是得实际验证。我们写个通用测试代码分别在JDK 6、JDK 8和JDK 17下运行看看结果稳不稳定public class StringPoolTest { public static void main(String[] args) { // 先让常量池里没有def这个字符串 String s1 new String(def); // 在已经new过之后再来看s1.intern()的返回值 String s2 s1.intern(); System.out.println(s1 s2); // 这里的结果视JDK版本而定 // 然后看普通的字面量赋值 String s3 def; System.out.println(s2 s3); // 这个理论上永远是true因为s2就是常量池里的对象 } }在JDK 6下s1.intern()发现常量池没有“def”会把s1的内容复制一份放到PermGen的常量池里返回的是复制后的新对象。所以s1 s2是false。在JDK 7及以上版本intern()直接记录s1的堆引用没有复制所以s1 s2是true。这个例子能直观展示JDK版本差异对字符串机制的影响。如果你面试时能把这一层变化讲清楚面试官基本能确认你是真的实战过、踩过坑而不是临时背了个面试题。4.3 常量池大小与GC的关系还有一个容易忽略的知识点JDK 7把字符串常量池移到堆里之后常量池里的字符串对象会参与GC。这一点和以前很不一样——当年PermGen里的字符串几乎是“永久”的不会被正常GC回收Full GC除外而且回收条件苛刻。现在常量池字符串变成了普通堆对象如果已经没有引用指向它GC就会把它回收掉。这带来一个实际影响测试intern()相关问题时如果开了不恰当的GC参数或者程序对内存特别敏感结果可能表现出不确定性。不过这是非常边缘的场景面试基本不会考但如果你在做性能优化时大量使用intern()就一定要意识到常量池里的字符串不是永生的它也会被GC回收。指望把一些动态生成的字符串intern()进去永久复用在内存紧张时可能并不靠谱。5. 面试官实际想挖的还不止这一道题5.1 变种一字符串拼接到底创建了几个对象聊完new String(abc)面试官经常接着问String str a b c;这行代码创建了几个对象坑点在于很多人下意识觉得拼接会产生多个中间字符串对象。但答案是编译期就直接被优化成String str abc了。Java编译器对常量字符串的拼接有优化——如果拼接的每个部分都是编译期常量字面量、final常量、常量表达式那么编译阶段就完成了拼接class文件里直接存放拼接后的结果字符串“abc”。也就是说运行时不涉及任何拼接操作ldc加载的就是“abc”。看字节码就一目了然0: ldc #2 // String abc 3: astore_1 4: return和直接写abc一模一样。所以回答如果常量池里没有“abc”就创建一个对象常量池里的“abc”如果已经有了就是0个新对象。但如果是变量拼接呢String a a; String b b; String c a b;这就完全不一样了。变量在编译期无法确定值所以编译器不会做常量折叠而是编译成类似这样的字节码先new StringBuilder()或者new StringBuffer()然后调用append方法最后调用toString()返回一个新的堆对象。所以在JDK 8及之前a b底层是StringBuilder拼接会创建一个StringBuilder对象和一个最终的String对象实际上还有中间步骤但关键对象就这两个。注意JDK 9之后字符串拼接改用invokedynamic实现不再强制依赖StringBuilder但效果也是创建一个新的字符串对象。这里如果面试官不问底层实现细节你答“会创建新的堆对象且不进入常量池”就够用了。5.2 变种二final修饰的字符串对拼接的影响这个知识点跟变种一紧密相关final String a a; final String b b; String c a b;由于a和b都是final修饰且在声明时初始化它们属于编译期常量。编译器在编译a b时能确定结果是“ab”所以同样会折叠成String c ab不会创建新的堆对象。这就是为什么很多公司代码规范里对于不变的字符串常量建议加上final——一方面语义清晰另一方面也方便编译器优化减少运行期无意义的对象创建。但如果final修饰的变量是在构造方法或方法里初始化的那它就不是编译期常量同样的拼接代码不会触发编译期优化。比如final String a; // 在某个方法里给a赋值 String c a b;这里a的值编译期未知运行时拼接仍然会创建新对象。5.3 变种三equals和到底怎么区分围绕字符串面试官还特别爱考equals与的区别。这个知识点对新手来说比较容易混淆但理解了对象的引用机制就不难比较的是两个引用变量指向的是不是同一个对象内存地址是否相同。equals比较的是两个对象的内容是否相等。String重写了equals所以只要内容相同就返回true。看几个典型例子String s1 abc; String s2 abc; String s3 new String(abc); System.out.println(s1 s2); // true因为s1和s2都指向常量池里的同一个abc System.out.println(s1 s3); // falses3指向堆里的新对象 System.out.println(s1.equals(s3)); // true内容相同这个例子完美复现了new String(abc)创建两个对象之后的比较逻辑。很多初学者会把s1 s3的结果记错其实就是没有理解“常量池对象”和“堆对象”是两个独立的实体。5.4 变种四构造方法传入的字符串也是字面量吗还有一种隐蔽的问法String s new String(new char[]{a, b, c});这里创建了几个对象注意传入的不是字符串字面量而是一个char[]数组。代码里没有出现字符串字面量所以在执行这行代码时常量池里不会主动创建“abc”。new String(char[])会在堆里创建一个基于字符数组内容的新字符串对象但这个对象不会自动加入常量池。所以这个例子通常只创建一个堆上的String对象不算那个char[]的话而且这个字符串不在常量池里。后续如果执行s.intern()常量池里才会正式出现“abc”。这也是new String(abc)和new String(new char[]{...})在对象创建数量上的本质区别前者构造参数是字符串字面量会触发常量池查询与可能的创建后者构造参数是字符数组不会触发常量池动作。5.5 变种五反射创建字符串最后一个变种面试如果聊到反射也可能顺势问一句String s String.class.getConstructor(String.class).newInstance(abc);其实用反射创建字符串的效果和new String(abc)一模一样因为参数“abc”仍然是字面量常量池的处理规则不变。但反射调用本身会多出来一些开销比如newInstance内部的访问检查所以实际性能比直接new差很多。这个例子不用多讲知道原理即可。6. 从这行代码延伸出的日常开发建议6.1 能不能用new String什么时候用先给结论95%以上的业务代码都不需要用new String(abc)。原因很简单字面量定义字符串可以复用常量池对象省内存。字面量写法可读性更好代码更简洁。无意义的new String会给堆内存增加不必要的垃圾对象加大GC压力。但确实存在需要new String的场景最常见的是从字符数组构造字符串时。比如从IO流读入一段字节转成字符数组然后用new String(bytes, charset)解码成字符串。这种情况下传入的不是字面量不存在“重复创建”的问题该用还是得用。还有一个场景是需要截取、清洗或转换字符串时比如new String(str.toCharArray(), offset, length)。不过这类操作在JDK 7以后有了更好的替代方案substring已经不会保留原字符数组引用所以现在很少真的需要手动new String了。6.2 判空和比较时最容易踩的坑日常开发中字符串判空或比较最常见的一个低级错误是if (str ) { // 想判断str是不是空字符串这是错的 }正确的写法if (.equals(str)) { // 或者 if (str ! null str.length() 0) { // 或者用工具类 // if (StringUtils.isEmpty(str)) }为什么str 是错的因为str如果来自new String()它指向堆对象而不是常量池对象比较的结果是false。虽然str 字面量赋值时确实会指向常量池对象但你不能保证调用方传进来的字符串是字面量还是new出来的。所以统一用equals比较内容才是安全的。另外我在排查线上问题时遇到过一种特殊情况从数据库或Redis取出来的字符串哪怕内容相同引用也几乎不可能和常量池对象相同。如果有人在代码里用比较从不同数据源取出的字符串基本都会踩坑。排查这类问题的通用思路是先看比较双方分别来自哪里再确认是内容比较还是引用比较然后检查常量池、堆、缓存各自的参与情况。6.3 生产环境真的遇得到这些问题吗可能有读者觉得这些面试题不是纸上谈兵吗生产环境谁会去比较字符串引用还真遇到过。有一次排查一个内存问题发现某个服务的内存占用持续走高。看堆转储后发现大量内容相同的字符串对象散落在堆里每个对象都几千上万份副本。原因就是业务代码里用new String或者字符串拼接变量拼接构造了大量重复字符串并且每份都作为独立的堆对象存活了下来。那个服务的代码长这样// 错误示例每次循环都new一个字符串 for (Order order : orderList) { String statusDesc new String(order.getStatus().getDesc()); resultMap.put(order.getId(), statusDesc); }order.getStatus().getDesc()返回的本身已经是字符串对象了完全没有必要再用new String包一层。看似小小的多余操作在订单量大、循环频繁的场景下就可能制造出大量无用的重复对象。解决方案一般有两种直接复用原对象不要new String包装。如果确实有大量内容相同且重复出现的字符串考虑用intern()主动把字符串驻留到常量池让后续相同的字符串能复用同一个引用。但要注意intern()本身也有性能开销需要查池只适用于“重复度极高”的场景。6.4 JVM参数与字符串池相关的实践虽然题目本身不涉及JVM参数调优但聊到字符串内存有几个参数值得知道JVM参数作用-XX:StringTableSize设置字符串常量池的桶数量默认在JDK 7是60013不同版本略有差异-XX:MaxMetaspaceSize限制元空间大小JDK 8间接影响动态类加载等场景-Xmx/-Xms堆大小参数常量池在堆里所以堆大小直接决定字符串常量池可用的空间在字符串大量使用intern()的程序里如果出现String.intern相关的性能瓶颈可以考虑调大-XX:StringTableSize来减少哈希冲突。我之前调过一次把桶数量从默认值调大之后intern()耗时明显下降。但这个参数要根据字符串种类数量来定不是越大越好具体可以在压测中对比不同值的表现。7. 我面试别人时想听到什么样的回答这几年作为面试官我面试Java候选人时也经常选择这道题作为起点。我大概会把回答分成几个层级第一层级背过答案型。回答“两个对象”问细节就说不清楚。这类候选人基础不牢大概率只是刷了题我不会给通过。第二层级理解机制型。能说清楚“常量池一个、堆一个”知道和equals的区别对intern()有基本了解。这类候选人基础OK可以进入下一轮。第三层级触类旁通型。能主动提到JDK版本差异、编译期优化、字符串拼接、StringBuilder的使用场景、intern()的适用条件和风险。这类候选人说明学习是有体系、有深度的我会重点考虑。所以你问我该怎么准备这道题别把它当成一道“背诵题”而是当成理解Java字符串机制的入口。把下面这些知识点串起来字符串常量池的位置与演进字面量与new的区别编译期常量折叠与运行期拼接的区别与equals的比较规则intern()的机制与坑字符串不可变性的意义。把这些都理清楚之后再回到String str new String(abc)这个问题上你会发现它根本不是一道“几个对象”的算术题而是一张考察Java基本功的网。理解到这个层面无论面试官怎么变着法子问你都不会慌。8. 几个实战中容易被忽略的边界情况最后分享几个我实际写代码或面试时遇到的边界情况这些细节最容易翻车。8.1 字符串常量池不是“永久”的前面提过JDK 7之后字符串常量池里的对象也会被GC回收。这意味着什么意味着一个字符串即使驻留在常量池里如果程序里再没有任何地方引用它它依然可能被回收。举个例子String s new String(temporary).intern(); // 手动把s置为null并且后续不再使用 s null; System.gc();虽然temporary在常量池里短暂存在过但如果之后没有其他引用GC还是可能回收它。所以不要假设“进了常量池就一劳永逸”在写缓存类代码时尤其要注意引用关系。8.2 循环里用拼接字符串的坑有一个偏性能的问题也容易出现在延伸讨论里String result ; for (int i 0; i 1000; i) { result i; }每次result i都会创建新的字符串对象和可能的StringBuilder对象1000次循环就会创建上千个对象。虽然编译器会把优化成StringBuilder但它在循环外只创建了一个StringBuilder每次循环都会toString()出一个新的不可变String然后下一次循环又得新建一个StringBuilder重新append之前的结果所以累计对象数量很多。正确做法是StringBuilder sb new StringBuilder(); for (int i 0; i 1000; i) { sb.append(i); } String result sb.toString();这就是为什么很多公司代码规范里明确禁止在循环内做字符串拼接。放在面试题语境下如果你能主动提到这一点会显得有实战经验。8.3 “对象个数”问题在传参时也适用这种情况稍微复杂一点void test(String param) { new String(param); }调用时传进来的param本身是个引用指向某个字符串对象。在new String(param)内部JVM拿着这个引用作为构造参数在堆里创建一个内容相同的新对象。此时创建对象数量取决于param指向的对象从哪来——是常量池、堆、还是别的什么地方。但核心机制不变new String(...)一定会在堆里创建新对象。这类方法在反编译框架、字节码处理库等底层代码中偶尔能看到业务代码里基本见不到。8.4 线程安全与字符串不可变性的关系最后补一个小知识面试官可能顺着字符串聊到不可变性和线程安全。String设计为不可变类意味着字符串对象一旦创建其内容就不可修改。这带来几个好处字符串对象可以安全地在线程间共享不需要加锁字符串可以被缓存包括哈希值缓存字符串可以安全地作为HashMap的key。理解了这些再看新New String那个堆对象和常量池对象的内容一致但身份不同也就更清楚为什么需要格外注意引用比较。我自己在日常开发里有个习惯代码review时只要看到和字符串相关的比较或构造都会多看一眼确认是内容比较还是引用比较、有没有无意义的new String。这个习惯帮我抓出过不少潜在的内存浪费和逻辑歧义问题。