Q1 String str = new String(“hello”); 之后,究竟产生了几个对象?
- 在解释这个问题之前,先解释四个概念——字面量与符号引用,常量池与字符串常量池。
1. 什么是字面量?
字面量可以通俗地理解为常量。比如文本字符串、数字、被声明为final的常量值。
Q1中的"hello",实际上就是文本字符串,属于字面量的范畴。
按数据类型分类,可以分为以下六种字面量。
整型字面量 | 123 |
---|---|
浮点型字面量 | 3.14F |
布尔型字面量 | true、false |
字符型字面量 | ‘a’ |
字符串字面量 | “abc” |
null字面量 | null |
- 什么是符号引用?
符号引用是编译原理中的概念,通俗地解释,包括了类和接口的全限定名、字段的名称和描述符方法的名称和描述符等。
- 什么是常量池?
常量池是Class文件中的一部分,常量池中主要存放两大类常量:字面量和符号引用。
- 什么是运行时常量池?
当类加载到内存中后,JVM就会将Class文件中的常量池中的内容存放到运行时常量池中,由此可知,运行时常量池是每个类都有一个。
- 什么是字符串常量池?
JDK 8版本下,在HotSpot虚拟机中,字符串常量池是堆区中的一个特殊的内存区域,用于存储字符串字面量的引用。当JVM加载一个类时,会将Class文件中的常量池中的内容存放到运行时常量池中,当首次使用某个字符串字面量时,在堆区中创建对应的字符串实例,并将其引用加入字符串常量池中。由于字符串常量池在堆区中,所以说字符串常量池是被一个JVM进程中的所有线程共享的。
在 Java 代码中,如果首次使用字面量的形式使用运行时常量池中的某个字符串字面量,则JVM会解析运行时常量池中的这个字符串字面量,具体来说,JVM会在堆区中创建这个字符串实例,并将其引用加入到字符串常量池中。如果这个字面量已经存在于字符串常量池了,则直接使用池中的字符串。
- 现在在回头看这段代码,仔细分析一下执行流程。
// step0 JVM加载当前类文件之后,"hello"这个字面量已经被加入到运行时常量池中
// step1 假设这行代码第一次使用了字面量"hello"
// JVM解析这个字面量,在堆区中创建字符串实例,将其加入字符串常量池
// "hello"引用字符串常量池中的字符串字面量。
String str = new String("hello");
//step2 接下来该执行String的构造方法了
// 这里传入的参数original实际上就是刚才的"hello"
// 可见,构造函数中,把"hello"的value、coder、hash属性拷贝到当前对象后就返回了
// 其中的value属性是一个被final修饰的byte数组:private final byte[] value;
// 这个value数组包含了original的五个码点,分别为h、e、l、l、o的unicode码点
// 从这里我们也能知道,String实际上是不可变的,是线程安全的
// 所以字符串常量池才能在被堆区中被各个线程共享
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
结论已经呼之欲出。String str = new String(“hello”);若"hello"字面量是第一次被使用, 这条语句会创建两个字符串对象,这两个字符串对象都在堆区中,其中的一个对象的引用被加入到字符串常量池中。如果"hello"字面量已存在于字符串常量池中,则只会创建一个对象。
Q2 String str = new String(“he”) + new String(“llo”);产生了几个对象?
- 先看String str = new String(“he”) + new String(“llo”);这条语句,根据Q1中的分析,我们知道,如果字符串常量池中不存在"he"和"llo",那么new String(“he”) 和new String(“llo”)分别会创建四个对象。其中的两个对象的引用被加入了字符串常量池。
- 在两个字符串都被new出来了之后,会进行字符串拼接。Java 语言并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。当某个表达式中第一次出现了字符串字面量,在其右边与其用加号连接的所有值会被转换成字符串。
TIP System.out.println(1+2+3+“math”+1+2+3); // 这条语句会输出6math123
- 在JDK 8环境下,多个字符串的拼接实际上是通过StringBuilder完成的。通过调试发现,在一条表达式中拼接多个字符串时,首先会创建一个空的StringBuilder对象,然后依次调用StringBuilder append(String);方法,每次传入一个String对象,完成拼接之后,这个StringBuilder对象会调用String toString();方法返回一个新的字符串。
举一个例子来说明字符串拼接的过程
@Test
public void testString(){
String s0="hello";
String s1="world";
String s2="hi";
String s3="Java";
// 首先创建一个空的StringBuilder对象
// 然后调用四次StringBuilder append(String);四次调用的参数分别为s0、s1、s2、s3
// 最终,这个StringBuilder对象调用toString方法,返回一个新的字符串
String s4= s0 + s1 + s2 + s3;
}
- 由以上分析可以得出,字符串拼接最终产生了一个新的字符串。
- 综上所述,String str = new String(“he”) + new String(“llo”);这条语句,在字符串常量池不存在"he"和"llo"的情况下,会创建5个对象。在字符串常量池存在"he"、"llo"其中之一的情况下,会创建4个对象。在字符串常量池存在"he"和"llo"的情况下,会创建3个对象。
TIP 可以看一下StringBuilder的String toString();方法,可以发现确实创建了新的String对象
// StringBuilder中的String toString()方法
public String toString() {
// Create a copy, don't share the array
return isLatin1() ? StringLatin1.newString(value, 0, count)
: StringUTF16.newString(value, 0, count);
}
// StringLatin1.newString(value, 0, count)
public static String newString(byte[] val, int index, int len) {
if (len == 0) {
return "";
}
return new String(Arrays.copyOfRange(val, index, index + len),
LATIN1);
}
// StringUTF16.newString(value, 0, count);
public static String newString(byte[] val, int index, int len) {
if (len == 0) {
return "";
}
if (String.COMPACT_STRINGS) {
byte[] buf = compress(val, index, len);
if (buf != null) {
return new String(buf, LATIN1);
}
}
int last = index + len;
return new String(Arrays.copyOfRange(val, index << 1, last << 1), UTF16);
}
Q3 String str = “he” + "llo"产生了几个对象?
//eg1. "he" + "llo"在编译期被编译器优化成字面量"hello"
String s0="hello";
String s1="hello";
String s2="he" + "llo";
System.out.println( s0==s1 ); //true
System.out.println( s0==s2 ); //true
// 字符串拼接中涉及变量,编译器无法优化
String s0 = "b";
String s1 = "a" + s0;
String s2 = "ab";
System.out.println(s1 == s2); //false
//示例5:final String s1 = "a";属于字面量,会被加入Class文件中的常量池
String s0 = "b";
final String s1 = "a";
String s2 = s1 + "b";
String s3 = "ab";
System.out.println(s3 == s2); //true
// 调用方法来初始化final String s1的值,编译器无法确定,无法优化
String s0 = "ab";
final String s1 = getB();
String s2 = "a" + s1;
System.out.println(s2 == s0); // false
private static String getB()
{
return "b";
}