String 和它的 intern
先做道题
Q1:定义了几个对象?
public class Q1 {
public static void main(String[] args) {
String s = new String("HelloWorld"); // 2 个(堆中 String 对象,常量池中字符串 "HelloWorld" 对象)
}
}
Q2:判断下面代码输出
public class Q2 {
public static void main(String[] args) {
String s1 = "HelloWorld";
String s2 = "Hello" + "World";
String s3 = new String("HelloWorld");
String s4 = s3.intern();
System.out.println(s1 == s2); // true,都指向常量池中字符串 "HelloWorld" 对象
System.out.println(s1 == s3); // false,符号引用 s3 指向堆中 String 对象
System.out.println(s1 == s4); // true,都指向常量池中字符串 "HelloWorld" 对象
}
}
一、String 字符串
String 表示字符串,不属于基本类型,但它可以像基本类型一样,直接通过字面量赋值
编译期
在编译期,符号引用和字面量会被加入到 Class 文件的常量池中,然后在类加载阶段,会进入常量池(常量池中已存在的字符串字面量不会重复添加)
常量池
Java 在内存分配中有 3种常量池:Class 常量池(静态常量池)、字符串常量池(存在于运行时常量池)、运行时常量池
- 常量池属于线程共享数据区
- 常量池可分为两大类:静态常量池(Class 常量池,存在于 Class 文件中)和运行时常量池
- 运行时常量池是方法区的一部分,存放一些运行时常量数据
字符串常量池
字符串常量池存在运行时常量池之中(JDK 6 及以前,存在方法区中;从 JDK 7 开始,存在于堆中)
二、intern() 方法(JDK 1.8)
/**
* ...
* 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.
* 调用 intern 方法时,如果池中已包含由 equals 方法确定的与此 string 对象相等的字符串,则返回池中的字符串
* 否则,此字符串对象将添加到池中,并返回对此字符串对象的引用
* ...
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
* 返回 一个字符串,其内容与此字符串相同,但保证来自唯一字符串池
*/
public native String intern();
上面 String#intern() 方法的注释中,其中一段所要表达的意思很明确:“调用 intern 方法时,如果池中已包含由 equals 方法确定的与此 string 对象相等的字符串,则返回池中的字符串。否则,此字符串对象将添加到池中,并返回对此字符串对象的引用”,就是说当 String 实例调用 intern() 方法时,JVM 会在字符串常量池中查询当前字符串是否存在,若存在,返回当前字符串;若不存在,则先将当前字符串放入常量池中,之后再返回
在 JDK 6 及以前版本中,字符串常量池里放的都是字符串常量;从 JDK 7 开始,字符串常量池从方法区移到了堆中,字符串常量池中也可以存放放于堆内的字符串对象的引用
三、字符串分析(JDK 1.8)
先用双引号创建字符串,再用一个 new 创建字符串
public class Test1 {
public static void main(String[] args) {
String str1 = "Test1";
String str2 = new String("Test1");
String str3 = str2.intern();
System.out.println(str1 == str2); // false
System.out.println(str1 == str3); // true
System.out.println(str2 == str3); // false
}
}
String str1 = “Test1”;
编译期:将字面量 Test1 会被加入到 Class 文件的常量池中
类加载阶段:字面量 Test1 会进入字符串常量池(注意:进入的条件是该常量此时不在字符串常量池,即不会重复添加相同常量)
str1 指向字符串常量池中字符串 “Test1” 对象
String str2 = new String(“Test1”);
编译期:在此之前字符串常量池中已存在 “Test1”,不会重复添加到字符串常量池
运行期:在堆中创建一个 String 对象
符号引用 str2 指向堆中创建的 String 对象
String str3 = str2.intern();
字符串常量池中已存在 “Test1”,调用 intern() 方法,不会重复添加到字符串常量池,直接返回字符串常量池中 “Test1” 对象
str3 指向字符串常量池中 “Test1” 对象
因此
str1(字符串常量池中字符串 “Test1” 对象)== str2(堆中创建的 String 对象):false
str1(字符串常量池中字符串 “Test1” 对象)== str3(字符串常量池中字符串 “Test1” 对象):true
str2(堆中创建的 String 对象)== str3(字符串常量池中字符串 “Test1” 对象):false
小结:
- 双引号创建的字符串在字符串常量池中
- 通过一个 new 方式创建字符串,不管该字符串是否已在字符串常量池中,intern() 调用前后两个符号引用指向的不是同一个对象
JVM 启动过程中加入常量池的字符串
上面的字符串 “Test1” 在字符串常量池中一开始并没有,但有的字符串在 JVM 启动过程中就已经存在,如:“Java”
public class Test2 {
public static void main(String[] args) {
String str1 = new String("ja") + new String("va");
String str2 = str1.intern();
String str3 = "java";
System.out.println(str1 == str2); // false
System.out.println(str1 == str3); // false
System.out.println(str2 == str3); // true
}
}
String str1 = new String(“ja”) + new String(“va”);
编译期:将字面量 ja 和 va 会被加入到 Class 文件的常量池中
类加载阶段:字面量 ja 和 va 会进入字符串常量池
运行期:在堆中创建三个 String 对象(两个匿名 String 对象,一个符号引号 str1 指向的 String 对象)
str1 指向堆中创建的 String 对象
String str2 = str1.intern();
编译期:在此之前字符串常量池中已存在 “java”,不会重复添加到字符串常量池(在 JVM 启动的时候会调用了一些方法,在常量池中会生成 “java” 等字符串常量)
str2 指向字符串常量池中的字符串 “java” 对象
String str3 = “java”;
编译期:在此之前字符串常量池中已存在 “Test1”,不会重复添加到字符串常量池
str3 指向字符串常量池中的字符串 “java” 对象
因此
str1(堆中创建的 String 对象)== str2(字符串常量池中的字符串 “java” 对象):false
str1(堆中创建的 String 对象)== str3(字符串常量池中的字符串 “java” 对象):false
str2(字符串常量池中的字符串 “java” 对象)== str3(字符串常量池中的字符串 “java” 对象):true
小结:
- 特殊情况:在 JVM 启动的时候会调用了一些方法,在常量池中会生成 “java” 等字符串常量(正常情况:下方 “### 先用两个 new 创建字符串”)
- JVM 启动过程中加入常量池的字符串,调用 intern() 方法,指向字符串常量池中的字符串对象
先用一个 new 创建字符串
public class Test3 {
public static void main(String[] args) {
// 情况一
String s1 = new String("Test3_1");
String s2 = s1.intern(); // intern() 在双引号之前调用
String s3 = "Test3_1";
System.out.println(s1 == s2); // false
System.out.println(s1 == s3); // false
System.out.println(s2 == s3); // true
System.out.println();
// 情况二
String s4 = new String("Test3_2");
String s5 = "Test3_2";
String s6 = s4.intern(); // intern() 在双引号之后调用
System.out.println(s4 == s5); // false
System.out.println(s4 == s6); // false
System.out.println(s5 == s6); // true
}
}
情况一
符号引号 s1 指向堆中创建的 String 对象(字符串内容是 “Test3_1”)
符号引用 s2 指向字符串常量池中的字符串 “Test3_1” 对象
符号引用 s3 指向字符串常量池中的字符串 “Test3_1” 对象
情况二
符号引号 s4 指向堆中创建的 String 对象(字符串内容是 “Test3_2”)
符号引用 s5 指向字符串常量池中的字符串 “Test3_2” 对象
符号引用 s6 指向字符串常量池中的字符串 “Test3_2” 对象
因此
s1(堆中创建的 String 对象,字符串内容是 “Test3_1”)== s2(字符串常量池中的字符串 “Test3_1” 对象):fasle
s1(堆中创建的 String 对象,字符串内容是 “Test3_1”)== s3(字符串常量池中的字符串 “Test3_1” 对象):fasle
s2(字符串常量池中的字符串 “Test3_1” 对象)== s3(字符串常量池中的字符串 “Test3_1” 对象):true
s4(堆中创建的 String 对象,字符串内容是 “Test3_2”)== s5(字符串常量池中的字符串 “Test3_2” 对象):fasle
s4(堆中创建的 String 对象,字符串内容是 “Test3_2”)== s6(字符串常量池中的字符串 “Test3_2” 对象):fasle
s5(字符串常量池中的字符串 “Test3_2” 对象)== s6(字符串常量池中的字符串 “Test3_2” 对象):true
小结:
- 通过一个 new 方式创建字符串,不管该字符串是否已在字符串常量池中,intern() 前后两个符号引用指向的不是同一个对象
先用两个 new 创建字符串
public class Test4 {
public static void main(String[] args) {
// 情况一
String s1 = new String("Test4_1") + new String("Test4_1");
String s2 = s1.intern(); // intern() 在双引号之前调用
String s3 = "Test4_1Test4_1";
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s2 == s3); // true
System.out.println();
// 情况二
String s4 = new String("Test4_2") + new String("Test4_2");
String s5 = "Test4_2Test4_2";
String s6 = s4.intern(); // intern() 在双引号之后调用
System.out.println(s4 == s5); // false
System.out.println(s4 == s6); // false
System.out.println(s5 == s6); // true
}
}
情况一
符号引号 s1 指向堆中创建的 String 对象(字符串内容是 “Test4_1Test4_1”)
符号引用 s2 指向字符串常量池中的引用,该引用指向 s1 引用的对象
符号引用 s3 指向字符串常量池中的引用,该引用指向 s1 引用的对象
情况二
符号引号 s4 指向堆中创建的 String 对象(字符串内容是 “Test4_2Test4_2”)
符号引用 s5 指向字符串常量池中的字符串 “Test4_2Test4_2” 对象
符号引用 s6 指向字符串常量池中的字符串 “Test4_2Test4_2” 对象
因此
s1(堆中创建的 String 对象,字符串内容是 “Test4_1Test4_1”)== s2(字符串常量池中的一个引用,该引用指向 s1 引用的对象):true
s1(堆中创建的 String 对象,字符串内容是 “Test4_1Test4_1”)== s3(字符串常量池中的一个引用,该引用指向 s1 引用的对象):true
s2(字符串常量池中的一个指向 s1 的引用)== s3(字符串常量池中的一个指向 s1 的引用):true
s4(堆中创建的 String 对象,字符串内容是 “Test4_2Test4_2”)== s5(字符串常量池中的字符串 “Test4_2Test4_2” 对象):fasle
s4(堆中创建的 String 对象,字符串内容是 “Test4_2Test4_2”)== s6(字符串常量池中的字符串 “Test4_2Test4_2” 对象):fasle
s5(字符串常量池中的字符串 “Test4_2Test4_2” 对象)== s6(字符串常量池中的字符串 “Test4_2Test4_2” 对象):true
小结:
- JDK 1.7 及以后,多个字符串变量拼接的字符串(如:通过两个 new 创建字符串),intern() 方法调用前,常量池中不存在拼接后的字符串
- JDK 1.7 及以后,多个字符串变量拼接的字符串(如:通过两个 new 创建字符串),intern() 方法调用后,常量池中不需要再存储一份对象了,可以直接存储堆中的引用(String#intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象)
四、总结
- JDK 1.7 及以后,字符串常量池从方法区移动到了堆中
- 双引号创建的字符串在字符串常量池中
- 通过一个 new 方式创建字符串,不管该字符串是否已在字符串常量池中,intern() 调用前后两个符号引用指向的不是同一个对象(调用 intern() 前后分别是:堆中 String 对象、字符串常量池中对应字符串对象)
- 特殊情况:在 JVM 启动的时候会调用了一些方法,在常量池中会生成 “java” 等字符串常量
- JVM 启动过程中加入常量池的字符串,调用 intern() 方法,指向字符串常量池中的字符串对象
- 通过一个 new 方式创建字符串,不管该字符串是否已在字符串常量池中,intern() 前后两个符号引用指向的不是同一个对象
- JDK 1.7 及以后,多个字符串变量拼接的字符串(如:通过两个 new 创建字符串),intern() 方法调用前,常量池中不存在拼接后的字符串
- JDK 1.7 及以后,多个字符串变量拼接的字符串(如:通过两个 new 创建字符串),intern() 方法调用后,常量池中不需要再存储一份对象了,可以直接存储堆中的引用(调用 String#intern() 方法时,如果存在堆中的对象,会直接在字符串常量池中保存堆中对象的引用,而不会重新创建字符串对象)