第一节 字符串常量池中的字符串
1.1 查找字符串常量池中的字符串
针对JDK1.6
结论1:当字符串常量池中存在相应字符串对象,则返回此对象。没有则返回空。
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- -----
针对JDK1.7
结论1:当字符串常量池中存在相应字符串对象或引用,则返回此对象或引用。没有则返回空。
注:引用是指堆中有多个相应字符串中的某个相应字符串的地址。因为堆中可以有多个相应字符串,但是字符串常量池只存储其中的一个的引用(具体存储那个就要看那个先调用的intern()方法了,这也是我们今天的主题,intern()方法的功能分析),每次查找都返回这一个引用。相应的,每次查找返回的对象也都是同一个。如下两图所示:
指向对象:
指向引用:
1.2 添加字符串常量池中的字符串
1.2.1 添加规则
JDK1.6
结论1:当字符串常量池中存在相应字符串对象,不创建,存在对象。
结论2:当字符串常量池中不存在相应字符串对象,创建并返回。
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- -----
JDK1.7
结论1:当字符串常量池中存在相应字符串对象或引用,不创建,返回此对象或引用。没有则返回空。
结论2:当字符串常量池中不存在相应字符串对象或引用,创建对象或添加引用,并返回。
注:JDK1.6不会存储引用,而是发现没有就在字符串常量池中新建一个相应字符串,这也是两个JDK的不同之处。
1.2.2 添加方式
1.2.2.1 声明字面量
方式1:
// code 1-1
String str = "Hello";
在没有其他代码的影响下,此时声明的Hello对象便只存在于字符串常量池中
1.2.2.2 String.intern()方法调用
方式1:
// code 1-2
String str1 = ...
String str1a = str1.intern();
注意:str1的产生方式有多种,对str1a的产生会有不同的影响,比如str1a是指向一个字符串常量池对象还是一个字符串常量池中存放的一个堆中对象的的引用,具体我们下文介绍。
1.3 推论
由上规则我们可以推导出:
推论1:字符串常量池中对某个字符串字面量的存储是唯一的。即同一个字符串,无论我们声明了多少次创建行为,在常量池只有一个相应对象或指向堆对象的引用,每个指向常量池中该字符串的变量都指向同一个对象。
第二节 new String(“Hello”) 到底创建了几个对象?
2.1 明确等价代码
在分析这个问题前,我们先明确一下 new String(“Hello”) 代码的等价代码,便于我们理解:
// code 2-1
String str1 = new String("Hello");
等价于:
// code 2-2
String temp = "Hello";
String str1 = new String(temp);
其实我们可以通过【code 2-2】观察得出,new String(“Hello”) 将创建两个对象。【code 2-1】中的"Hello"是先在字符串常量池中创建了匿名的字符串,对应【code 2-2】则是创建了一个temp指向的字符串。接着,当我们执行new String(temp)时会在堆中创建一个字符串对象。故此时创建了两个对象。
这里涉及到了“字符串字面量(String literal)”和“字符串对象(String object)”的相关概念:简单来说,我们通过String temp = "Hello"创建的temp就是运用了字符字面量来创建字符串,而String str1 = new String(“Hello”)则是创建了一个字符对象。区别在于temp永远在字符串常量池中,而str1永远在堆中。参考1,参考2。
2.2 结论
我们现在知道当我们new String(“Hello”)时会创建两个对象。但是有个小问题:当字符串常量池中以及存在"Hello"时,还会创建两个对象吗?不会。此时,因为字符串常量池中已经存在了对应字符串,此次只会在堆中创建一个对象。因此,我们可以得出结论:
结论1:当字符串常量池中【已】存在相应字符串时:new String(“Hello”)会创建一个对象;此时会在堆中创建一个新的对象。
结论2:当字符串常量池中【不】存在相应字符串时:new String(“Hello”)会创建两个个对象;此时会在堆和栈中分别创建对象。
有个小问题:当我们执行String str1 = "Hello";
时,我们创建了几个对象?
同理,具体创建几个应该应该根据当前字符串变量池中是否存在"Hello"来判断:
推论1:当字符串常量池中【不】存在对应字符串时,此时创建一个对象。
推论2:当字符串常量池中【已】存在对应字符串时,此时不创建对象。
另外,JVM在启动时就会生成一些字符串并存储在字符串常量池中。如"java"、"a"等等(这会为我们接下来分析String.intern()方法带来影响,我们接下来再分析)。比如sun.misc.Version类中定义了一些final类型的字符串,因为JVM启动时回加载这个类,所以在我们执行自己的String str = "java"时并不会创建对象【至于final为什么会在字符串常量池中创建字符串,我们第三节会分析】。参考以下JDK代码:
// code 2-3
package sun.misc;
public class Version {
private static final String launcher_name = "java";
private static final String java_version = "1.8.0_212";
private static final String java_runtime_name = "Java(TM) SE Runtime Environment";
private static final String java_profile_name = "";
private static final String java_runtime_version = "1.8.0_212-b10";
//忽略了其他代码
}
当然,我们自己定义的final类型的使用字符串字面量创建的字符串也会产生相同影响。
2.3 扩展
2.3.1 几种不同的字符串创建方式
2.3.1.1 只在于字符串常量池中创建一个字符串对象的代码
代码如下
// code 2-4
String str1 = "Hello";
2.3.1.2 既在字符串常量池中创建,也在堆中创建代码
方式1:
// code 2-5
String str1 = new String("Hello");
2.3.1.3 只在堆中创建字符串的代码
代码如下:
// code 2-6
String str = new String("Hello") + new String("World");
有个小问题:为什么new String("Hello") + new String("World");
不在字符串常量池中创建一个对象?
因为此种情况下"+"使用了StringBuilder.append(“Hello”).append(“World”).toString()完成创建,并没有 String temp = "HelloWorld"这样的临时变量产生。
其实new String(“Hello”)的时候并不是JVM自动在字符串常量池中创建了"Hello",而是因为new String(“Hello”)中"Hello"这个匿名的字符串字面量存在才导致了在字符串常量池中创建了"Hello";我们可以验证一下:
// code 2-7
char[] c = {'H', 'e', 'l', 'l', 'o'};
String s = new String(c);
String s1a = s.intern();
String sa = "Hello";
System.out.println(sa == s);
System.out.println(s1a == sa);
System.out.println(s1a == s);
分析:s一定是在堆中,由 sa == s 可知 sa 必定指向堆。若此时字符串常量池已存在"Hello"则 s != sa,故当我们不用 new String(“Hello”)形式创建字符串对象时,并不会在字符串常量池中创建对象。s1a == s 且 s1a == sa 也表明了在执行intern()方法时得到是一个在堆中的引用(JDK1.7 中String.intern()实现方式,1.6中会复制一个字符串到字符串常量池中),即此时字符串常量池不存在"Hello"对象而只存在一个指向堆"Hello"对象的引用。
我们将c改为显示的"Hello",再来看一个代码:
// code 2-8
String s = new String("Hello");
String s1a = s.intern();
String sa = "Hello";
System.out.println(sa == s);
System.out.println(s1a == sa);
System.out.println(s1a == s);
结果:
分析:由于代码String s = new String(“Hello”)中存在一个匿名的"Hello",所以此代码执行完后会在堆中创建一个"Hello"对象,并且也在字符串常量池中创建一个"Hello"。此时s指向堆中Hello,字符串常量池中的"Hello"无人引用。当执行String s1a = s.intern()时,会先从字符串常量池中寻找"Hello"对象或指向堆中"Hello"对象的引用。此时字符串常量池已经创建了一个"Hello"对象,故将此对象返回,因此有s1a != s 。当执行String sa = "Hello"时同理先去字符串常量池中寻找"Hello"对象或指向堆中"Hello"对象的引用,又将刚刚字符串常量池中的"Hello"返回,所以有 s1a == sa 为 true,且 sa != s 。
通过【code 2-7】和【code 2-8】即可证明创建两个对象是因为字符串常量池中对象是我代码中声明了匿名的对应字符串变量导致的。
回到我们原本的问题,那么代码String str1 = new String("Hello") + new String("World")
会在字符串常量池中创建对象吗?
根据new String("Hello")
的结论,并不会,因为我们并没有声明显示或隐式的"HelloWorld"字符串,故此时只会在堆中创建对象str1。我们再看一下上面的图理解一下:
第三节 Java中字符串拼接【+】的实现方式
3.1 常量字符串之间的"+"操作,编译阶段直接会合成为一个字符串。
常量字符串之间的“+”操作,编译阶段直接会合成为一个字符串。如String str=”GM”+”TH”,在编译阶段会直接合并成语句String str=”GMTH”,于是会去常量池中查找是否存在”GMTH”,从而进行创建或引用。
常量字符串之间的"+"操作不会在堆上创建对象(TODO会不会引用堆上的对象呢?会)
// code 3-1
String str = "GM" + "TM";
// 等价于:
String str = "GMTM";
3.2 对于final字段,编译期直接进行了常量替换(而对于非final字段则是在运行期进行赋值处理的)。
// code 3-2
final String str1 = "OM";
final String str2 = "PT";
String str3 = str1 + str2;
String str4 = "OMPT";
System.out.println(str3 == str4); //true
在编译时,直接替换成了String str3=”OM”+”PT”,根据2.1,再次替换成String str3=”OMPT”,再根据1.3的推论1,str4不会新创建对象,而是直接引用str3对应的字符串,所以 str3 == str4 结果为 true。如下图:
有个小问题,这时候字符串常量池中我们一共创建了几个对象呢?
3.3 常量字符串和变量拼接时,会调用StringBuilder.append()在堆上创建新的对象。
常量字符串和变量拼接时(如:String str3=baseStr + “01”)会调用stringBuilder.append()在堆上创建新的对象。
我们使用code 4的代码变体,将str1声明的final去除,如下:
// code 3-3
String str1 = "OM";
final String str2 = "PT";
String str3 = str1 + str2;
String str4 = "OMPT";
System.out.println(str3 == str4); //false
结果:
我们再来看看另外一种代码变体:
//code 3-4
String str1 = "OM";
String str3 = str1 + "PT";
String str4 = "OMPT";
System.out.println(str3 == str4); //false
结果:
再来一种:
// code 3-5
String str1 = new String("OM");
String str3 = str1 + "PT";
String str4 = "OMPT";
System.out.println(str3 == str4); //false
结果:
字符串对象和字符串对象之间的“+”:
// code 3-6
String str1 = new String("OM");
String str2 = new String("PT");
String str3 = str1 + str2;
String str4 = "OMPT";
System.out.println(str3 == str4); //false
结果:
还是那个小问题,以上几种情况分别创建了几个对象呢?(提示:包含字符串常量池和堆里所有的对象创建)
第四节 intern()方法的作用
4.1 intern()方法的本质
偷个懒,直接给结论:
1.intern()方法的本质其实就是在字符串常量池中生成一个字符串对象!为了给应用程序重复引用,因为一个字符串在字符串常量池中只会有唯一一个对象。
2. 在JDK1.7中,不仅可以存放对象,还可以存放一个堆中字符串对象的引用,标识当前字符串常量池中已经存在了某个字符串。故我们从字符串常量池中获得的引用可能是一个对象,也可能是一个堆中对象的引用。
3.当字符串常量池已经存在某个字符串对象或引用时,其他所有在常量池中获取此字符串的操作都将返回这对象或引用。
4. 当字符串常量池中不存在某个字符串的对象或引用时,此时我们需要创建字符串对象或将堆中相应对象的引用放到字符串常量池中。
5.在字符串常量池中创建一个字符串对象或者或者存放一个堆中字符串引用的方式有两种:a.使用String str1 = "Hello";
b.使用String.intern()
方法。
6.如何判断当前字符串常量池是否已存在对应字符串的对象或引用?如何判断当前应该创建对象还是将堆中相应对象的引用放到字符串常量池中?这两个问题其实是困扰我们最大的问题。但是这些问题我们都可以通过本文的分析给出答案。
4.2 JDK 1.6 中的 String.intern() 方法的行为
4.2.1 String.intern()行为定义
JDK 1.6 字符串常量池在永久代。
String.intern() 的行为:
结论1:当字符串常量池中【已】存在相同字符串时,直接返回这个在字符串常量池中的字符串。
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- -----
结论2:当字符串常量池中【不】存在相同字符串时,则在字符串常量池中创建字符串,然后返回这个字符串常量池中的字符串。
我们可以用以下伪代码来描述这个逻辑(假设我们操作的字符串对象是"Hello"):
// fake code 4-1
if (字符串常量池中已存在 "Hello" 对应字符串){
// mark 0
return 字符串常量池 中的 "Hello";
} else{
// mark 1
// 在 字符串常量池 创建一个 "Hello"
字符串常量池.create("Hello");
return 字符串常量池 中的 "Hello";
}
4.2.2 情形分析
【mark 0】的情况:
【mark 1】的情况:
字符串常量池中的"Hello"对象为什么是 new String(“Hello”)生成的?参考上文的结论可以解决这个问题。
【mark 1】的另一种情况:
【mark 1】的再一种情况:
由此,我们可以得到一个推论:
推论1:String.intern()方法返回的永远是字符串常量串中字符串对象,而不可能是堆中的字符串对象。
4.3 JDK 1.7 中的 String.intern() 方法的行为
4.3.1 String.intern()行为定义
JDK 1.7 字符串常量池在堆中。
String.intern() 的行为:
结论1:当字符串常量池中【已】存在相同字符串【或】此字符串对应的引用时,直接返回字符串常量池中相同字符串【或】此字符串对应的引用。
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- -----
结论2:当字符串常量池中【不】存在相同字符串【或】此字符串对应的引用时,将intern()方法的调用者的引用,存储在字符串常量池中;然后返回此引用。
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- -----
注:【此字符串对应的引用】这个引用指向的对象应该是来自于堆中一个字符串对象。
1.如何创建一个这样的引用对象?(有几种方式)
2.
以上结论可用如下伪代码表示:
// fake code 4-2
if (字符串常量池中已存在 "Hello" 对应字符串 or 对应的引用){
// mark 0
return 字符串常量池 中的 "Hello" 对应字符串 or 对应的引用;
} else {
// mark 1
字符串常量池.put(intern()方法的调用者的引用);
return 字符串常量池 中的 intern()方法的调用者的引用;
}
4.3.2 情形分析
还记得我们在第一节中的到的结论吗?
当使用new String("Hello")
创建字符串对象时,分别在堆和栈中创建了一个对象。我们来验证一下String.intern()的逻辑是否符合我们的描述。
如下代码:
// code 4-3
String str1 = new String("Hello");
String str1a = str1.intern();
System.out.println(str1 == str1a); //false
String str2 = "Hello";
System.out.println(str2 == str1a); //true
System.out.println(str1 == str2); //false
String str2a = str2.intern();
System.out.println(str2 == str2a); //true
结果:
第一的false:因为new String("Hello")
创建了两个对象,当调用String str1a = str1.intern()
时此时字符串常量池中已经存在了一个"Hello",所以str1a执行字符串常量池而str1指向堆,故此时为false;此时走的是【mark 0】的逻辑。
第二的true:此时的String str2 = “Hello"语句,根据我们【2.2】的两个推论,它并不会新建对象,而是在字符串常量池中寻找到了new String(“Hello”)创建时在字符串常量池中创建的"Hello”,故此时 str2 == str1a 为 true。
第三的false:当我们执行String str1 = new String("Hello")
时,str必定指向堆;根据【第二的true】的分析我们知道str2在字符串常量池中,故此时 str1 != str2。
第四的true:根据刚刚的结论,str2a返回字符串常量池中已存在的"Hello";其实此时str1a,str2,str2a都指向同一个字符串常量池中的字符串。
我们再来看一段代码:
// code 4-4
String str1 = new String("Hello") + new String("World");
String str1a = str1.intern();
System.out.println(str1 == str1a); // true
String str2 = "HelloWorld";
System.out.println(str1 == str2); //true
System.out.println(str2 == str1a); //true
结果:
第一个true:在执行String str1 = new String("Hello") + new String("World")
时,根据第2节可以判断 str1 指向一个堆中的"HelloWorld"对象,当我们执行String str1a = str1.intern()
时根据伪代码中的判断条件我们走到了【mark 1】,此时堆中有个"HelloWorld"对象而且字符串常量池中有一个对堆中"HelloWorld"的引用。str1a即指向了这引用,即str1a和str1都是指向了堆中的"HelloWorld"对象。故有 str1a == str1 为 true。
第二个true:执行String str2 = "HelloWorld"时会先去字符串常量池中寻找是否有"HelloWorld"字符串对象或者引用堆中某个"HelloWorld"的引用,此时字符串常量池中存在一个引用str1的对"HelloWorld"的引用,故返回这个引用,可知,str2 == str1 为 true。此时 str1、str2、str1a指向同一个堆中的"HelloWorld"对象。
第三个true:根据【第二个true】的分析我们很容易得到 str2 == str1a 成立。
我们通过这个分析可以得出:
结论3:在 JDK1.7 中,字符串常量池中查找某个字符串时是否存在时,如果字符串常量池中存在相应字符串的对象或者其在堆中的引用,那么便会返回这个对象或引用。
结论4:在 JDK1.7 中,通过字符串常量池中查找某个字符串是否已经创建,返回的可能是字符串常量池中的一个对象或者堆中相应对象的引用。
延伸一下:
如果执行两次 new String(“Hello”) + new String(“World”),返回了两个引用,那么这个两个引用相同("==")吗?引用1.intern()和引用2.intern() 相同("==")吗?如果相同那么这个引用.intern()和引用1还是引用相同呢?还是都不相同呢?
下面我们来验证一下:
// code 4-5
String str1 = new String("Hello") + new String("World");
String str2 = new String("Hello") + new String("World");
System.out.println(str1 == str2); //false
String str2a = str2.intern();
String str1a = str1.intern();
System.out.println(str1a == str2a); //ture
System.out.println(str1a == str1); //false
System.out.println(str2a == str2); //ture
String str3 = "HelloWorld";
System.out.println(str3 == str1a); //ture
System.out.println(str3 == str2a); //ture
System.out.println(str3 == str1); //false
System.out.println(str3 == str2); //ture
结果
第一的false:str1和str2分别是堆中的两个对象,它们当然不同,故 str1 != str2。
第二的true:当str2执行intern()方法时,此时栈中没有"HelloWorld"字符串对应的值,根据伪代码的【mark 1】,故将str2的引用放到字符串常量池中,并返回这个引用给str2a。当str1执行.intern()方法时,会找到栈中str2的引用。即它们intern()方法执行后返回的是同一个引用,故有 str1a == str2a 为true。
第三的false:根据【第二的true】的分析,我们知道str1a == str2,而根据【第一的false】我们知道 str1 != str2。故 str1a != str1 。
第四的true:根据【第二的true】可得str2a == str2 为 true。
第五的true:当执行String str3 = "HelloWorld"时,JVM会首先在字符串常量池中寻找是否存在"HelloWorld"字符串对象或者堆中某个对应对象的引用。此时在字符串常量池中发现了str2的引用并返回给str3,故此时有 str3 == str2,根据【第四的true】可知 str2 == str2a ,根据【第二的true】可知 str2a == str1a,故可以得出str3 == str1a 为 true.
第六的true:根据【第五的true】我们知道 str3 == str1a,再根据【第二的true】可知 str1a == str2a。故可以得出 str3 == str2a;
第七的false:根据【第六的true】可知 str3 == str2a,根据【第四的true】可知 str2a ==str2,根据【第一的false】可知 str2 != str1。故 str3 != str1。且str3 == str2 为true。
第八的true:根据【第七的false】可得 str3 == str2 为 true。
分析完了,有个小问题,如果String str3 = “HelloWorld” 在代码开始时执行,那么结果会发生什么变化呢?其他我们可以根据以上的结果结论和推论来得到结果,试一试吧,然后用代码验证一下自己的结果!
4.4 结论
结论在【4.1】已经给了。那么说一下我们遇到此类相关问题时如何进行分析吧。
4.4.1 首先你要知道这个变量在哪儿?字符串常量池?堆中?
如何判断呢?我们可以通过变量的创建方式来推测。
一般创建方式有以下几种:
4.4.1.1 JVM启动时已经在字符串常量池还在那个初始化的字符串
比如 “java”、"Java™ SE Runtime Environment"等等。这些已经在字符串常量池中的字符串不会在被我们重新再字符串常量池中创建或引用到堆中对象。
注:【参考2.2】
4.4.1.2 变量的创建方式
// code 4-6
// mark 1
// 此时字符串常量池中有str1,堆中为空
String str1 = "Hello";
// mark 2
// 此时字符串常量池中有"Hello",但是无人引用,堆中有str2
String str2 = new String("Hello");
// mark 3
// 此时字符串常量池中有"Hel"、"lo",但是str3在堆中
String str3 = new String("Hel") + new String("lo");
// mark 4
// 此时字符串常量池为空,str4在堆中
char[] c = {'H','e','l','l','o'};
String str4 = new String(c);
// mark 5
// 【参考3.1】同 mark 1
String str5 = "Hel" + "lo";
// mark 6
// 【参考3.2】同 mark 1
final String temp = "Hel";
String str6 = temp + "lo"
// mark 7
// 【参考3.2】同 mark 1
final String temp1 = "Hel";
final String temp2 = "lo";
String str7 = temp1 + temp2;
// mark 8
// 【参考3.3】同 mark 3 此时字符串常量池中有"Hel"、"lo",此时str8在堆中
String temp = "Hel";
String str8 = temp + "lo";
// mark 9
// 【参考3.3】同 mark 3 此时字符串常量池中有"Hel"、"lo",此时str9在堆中
String temp1 = "Hel";
final String temp2 = "lo";
String str9 = temp1 + temp1;
// mark 10
// 【参考3.3】同 mark 3 此时字符串常量池中有"Hel"、"lo",此时str10在堆中
String temp = new String("Hel");
String str10 = temp + "lo";
我们看到不同的创建方式创建的对象的位置不一。还需要注意一点:我们创建字符串对象的过程中到底创建了几个对象?
4.4.1.3 我们创建字符串对象的过程中到底创建了几个对象?
需要我们注意的是:当我们以为我们在堆中创建了一个对象,实际我们还在字符串常量池创建了一个相同的字符串对象,它会影响我们调用String.intern()方法的返回值。
注:【参考 第二节】。
搞清楚了我们创建的字符串变量的位置,已经我们创建了多少对象。我们就可以分析String.intern()方法生成的变量到底指向哪儿了!
4.4.1.4 String.intern()方法生成的变量到底指向哪儿?
String.intern() 就可以生成一个新的变量引用,新的变量引用可能和调用方的相同,也可能不同,我们接下来分析。
4.4.1.4.1 JDK1.6 String.intern()方法返回的变量到底指向哪儿?
如果字符串常量池中【已】存在相应的字符串对象,则返回这对象。此时返回的变量指向字符串常量池。
如果字符串常量池中【不】存在相应的字符串对象,则在字符串常量池创建这个对象,返回刚刚创建的对象。此时返回的变量指向字符串常量池。
注:参考【4.2】
4.4.1.4.2 JDK1.7 String.intern()方法返回的变量到底指向哪儿?
如果字符串常量池中【已】存在相应的字符串对象或引用,则返回此对象或引用。此时返回的变量指向字符串常量池或堆。此时我们查找该字符串或其引用第一次被放入字符串常量池的操作(两种操作方式【参考1.2.2】)的位置,即可确定该字符串的信息。
如果字符串常量池中【不】存在相应的字符串对象,则此时调用方必定是在堆中,因为如果是一个字符串常量池中的对象调用intern(),那么这个对象已经在字符串常量池中了,不会进入此逻辑;此时会将调用方的引用放到字符串常量池中,返回刚刚的引用。此时返回的变量指向堆中调用方。
我们可以看看【code 4-5】,一个很好的例子。
4.4.2 总结一下
1.搞清楚JVM启动时字符串常量池中已经有哪些字符串了。
2.搞清楚我们创建几个字符串对象,是否在堆和字符串常量池中都创建了
3.搞清楚JDK1.7是可以把堆对象放进去的,而且放进去后就不会再在字符串常量池新建相应字符串对象了;堆中同一个字符串字面量对应的对象可能有多个,怎么知道是字符串常量池的引用指向哪个?当然是【先到先得】了。如何将堆对象的引用放到字符串常量池中呢?使用String.intern()方法喽。
4.搞清楚当我们调用了String.intenr()方法后返的值指向哪儿?字符串常量池还是堆?如果是堆,堆中如果有多个对象对应相同的字符串字面量,那么此时返回值具体指向哪一个?
5.搞清楚以上的问题。
转载:https://blog.csdn.net/qq_14947845/article/details/105874912