在学习过程中对String类的理解反复刷新,以此文记之,做归纳总结,也适合新手避坑。
以实用性考虑,环境为Java 8 以及 之后版本。
String类相比其它类特殊的地方在于有一个字符串常量池(StringTable),里面存着字面量的引用。
(关于是实例还是引用的争论请看下方链接,个人站引用)
JVM 常量池中存储的是对象还是引用呢?
达成这个共识以后,我们从最简单的例子开始说起
对象创建
String str = "abc";
"abc"这个String对象是怎么创建的呢?
①在StringTable中查找,若有内容匹配的引用,则直接返回这个引用。若无内容匹配的String实例的引用,则在Java堆里创建一个对应内容的String对象,然后在StringTable记录下这个引用,并返回这个引用。
Java 中new String(“字面量”) 中 “字面量” 是何时进入字符串常量池的?
(感谢此回答,让我受益匪浅)
请留意这个过程,这个和后续提到的intern方法像亲兄弟一样。
入门问题一
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);
// 很简单 true
// 内存中只有一个 "hello" 对象被创建,同时被 s1 和 s2 共享。
现在将创建对象变得复杂一点
String str2 = new String(“abc”);
String str2 = new String(“abc”); 与 String str1 = “abc”; 有什么区别?
②因为此行代码构造新字符串要用到字面量"abc",所以要先完成"abc"这个对象的创建(见①),再完成另一个String对象的创建,str2指向后面这个实例。
注意1,我原先写的是“要先创建"abc"这个对象(见①),再new另一个String对象。”后来觉得不严谨,因为"abc"的开始创建比另一个晚,但是完成另一个早,所以换成了上述说法,若要深究,请移步。
深入理解 new String()
注意2,两个实例都在堆中。str2的value指向"abc"的value,两个对象的char[] value是一致的。(JDK9以后是byte[])
这就有了一个经典问题二
String str2 = new String(“hello”); 在内存中创建了几个对象?
由②可知,2个。
结合①和②,很容易解决问题三
String s1 = "javaEE";
String s2 = "javaEE";
String s3 = new String("javaEE");
String s4 = new String("javaEE");
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//false
System.out.println(s1 == s4);//false
System.out.println(s3 == s4);//false
"+"号拼接字符串
有变量参与的拼接和只有常量参与的拼接是不一样的。
只有常量参与的拼接有以下两种情况
全是字面量拼接
String s2 = "abc" + "123";
final修饰的常量拼接
final String s = "abc";
String s2 = s + "123";
上面均和String s2 = "abc123"是等价的,因此s2的创建过程参考①。
一旦有变量参与拼接,例如
String s = "abc";
String s2 = s1 + "123";
或
String s2 = new String("abc") + new String("123");
java会通过StringBuilder来进行字符串的拼接,通过append()方法,最后直接toString()返回。而这个toString()方法调用的其实是String(byte[] value, byte coder)这个构造器。注意这个构造器,传入的是字节数组而不是字符串"abc123",就不会创建"abc123"实例并且也不会在StringTable中记录,这意味着"abc123"的引用在StringTable中是找不到的! 接着,既然是调用的构造器,自然是在堆中new一个新对象。
结合上述说明很容易解决问题四和五
问题四
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";
String s4 = s1 + "world";//s4 字符串内容也 helloworld , s1 是变量, "wo
rld" 常量,变量 + 常量的结果在堆中
String s5 = s1 + s2;//s5 字符串内容也 helloworld , s1 和 s2 都是变量,
变量 + 变量的结果在堆中
String s6 = "hello" + "world";// 常量 + 常量 编译期间就可以确定结果
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//true
问题五
final String s1 = "hello";
final String s2 = "world";
String s3 = "helloworld";
String s4 = s1 + "world";//s4 字符串内容也 helloworld , s1 是常量, "wo
rld" 常量,常量 + 常量结果在常量池中
String s5 = s1 + s2;//s5 字符串内容也 helloworld , s1 和 s2 都是常量,
常量 + 常量 结果在常量池中
String s6 = "hello" + "world";// 常量 + 常量 结果在常量池中,因为编译
期间就可以确定结果
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//true
System.out.println(s3 == s6);//true
问题六
String str = "hello";
String str2 = "world";
String str3 ="helloworld";
String str4 = "hello".concat("world");
String str5 = "hello"+"world";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
concat 方法也是调用String(byte[] value, byte coder)这个构造器,哪怕是两个常量对象拼接,结果也是在堆。
问题七
String s1 = new String("1") + new String("1");
String s2 = new String("1") + "1";
String s3 = new StringBuilder("1").append("1").toString();
String s4 = "1" + "1";
System.out.println(s1 == s4); //false
System.out.println(s2 == s4);//false
System.out.println(s3 == s4);//false
问题七你做对了,但可能还是有如下误区
误区一
过去我总以为,只要一个字符串对象被new出来,在StringTable中是存在它的字面量引用的,这是错误的。 比如:
String str = String s1 = new String("a") + new String("bc");
此时StringTable中会有"abc"吗?
不会的,StringTable中只有"a"和"bc"。
原因在加号拼接那一节讲过,不再赘述,这里再提,是想重点强调一下,这对后面很重要。
那么问题来了
什么情况下,字符串引用会被存入字符串常量池?
只有下面四种情况
1.字符串字面量:
String s1 = "Hello";
这个字符串 “Hello” 引用会被存入字符串常量池。
2.字符串连接(编译时常量):
String s2 = "Hello" + " World"; // 编译器在编译时将其优化为 "Hello World"
这种情况下,“Hello World” 引用也会存入常量池。
3.常量表达式:
final String prefix = "Hello"; String s4 = prefix + " World";
// 由于 prefix 是 final,编译器可以优化并引用到常量池
4.使用 String.intern() 方法:
String s3 = new String("Hello").intern();
如果常量池中已经存在 "Hello"引用,则返回常量池中的 "Hello"引用;如果不存在,则将当前"Hello"对象的引用添加到常量池中。
前三种之前说过,下面重点解释第四种
先看看注释
When the intern method is invoked, if the pool already contains a
string equal to this String object as determined by the equals(Object)
method, then the string from the pool is returned. Otherwise, this
String object is added to the pool and a reference to this String
object is returned.
若能在StringTable中找到字面量一样的对象引用,返回此引用。若不能找到,将调用对象的引用作为当前字面量的引用存入StringTable中,并返回此引用。
误区二
过去我总以为,调用intern方法时,如果在StringTable中不能找到字面量的引用,会新建一个字面量对象,并把当前对象的引用加入StringTable(眼熟不,这就是字面量String对象的创建过程啊),这是错误的。真实情况是,正如上面所说,不会创建新对象,而是将调用对象的引用作为当前字面量的引用存入StringTable中,并返回此引用。
也正是因为我带着误区二的观点,认为下面的结果是false,但实际上是true。在纠正过来以后,结果是true彻底说通了。
String a = "a";String param = new String("param" + a);String intern = param.intern();String paramSame = "parama";System.out.println(param == paramSame);//true
由此可以轻松解决问题八
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";
String s4 = (s1 + "world").intern();// 在上一行已经将"helloworld"的引用放入StringTable中了
String s5 = (s1 + s2).intern();
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//true
问题九
String s1 = new String("1") + new String("1");
s1.intern();
String s2 = "11";//在上一行已经将"11"的引用s1放入StringTable中了
System.out.println(s1 == s2);//true
理解了上文所有的加粗文字后,可以解决知乎上的一众疑惑(对他们问题中的代码结果做出合理解释)
new一个String对象的时候,如果常量池没有相应的字面量真的会去它那里创建一个吗?我表示怀疑。
问题十
String a = "a";
String param = new String("param" + a);
String paramSame = param.intern();
System.out.println(param == paramSame);//true
//调用param.intern();的时候,在StringTable中将param作为"parama"的引用,并且返回param,这两者当然是相等的。
问题十一
String a = "a";
String param = "b" + a;
System.out.println(param.intern() == "ba"); //true
//先调用param.intern(),将param作为"ba"的引用存入StringTable,
//再遇到"ba"时,StringTable中已有"ba"的引用,不再创建新对象,直接返回,此时"ba"的引用是param
System.out.println(param == "ba");//true
问题十二
String a = "a";
String param = "b" + a;
System.out.println("ba" == param.intern()); //true
//StringTable中没有"ba"的引用,会先创建对象,并返回引用。再执行param.intern()时,发现已经有"ba"的引用,直接返回。
//因此相等
System.out.println(param == "ba");//false
//这是两个不同对象的引用
Java 中new String(“字面量”) 中 “字面量” 是何时进入字符串常量池的?
问题十三
String s1=new String("he")+new String("llo"); s1.intern(); String s2="hello"; System.out.println(s1==s2);//true
问题十四
String s1=new String("he")+new String("llo");String s2=new String("h")+new String("ello");String s3=s1.intern();String s4=s2.intern();System.out.println(s1==s3);//trueSystem.out.println(s1==s4);//true
最后两个问题,我就不解释了,按照上文的规则,都能说得通,可以好好想想,累了。