直接定义过程:
String s1="myString";
在程序编译期,编译程序先去字符串常量池检查,是否存在“myString”,如果不存在,则在常量池中开辟一个内存空间存放“myString”;如果存在的话,则不用重新开辟空间。然后在栈中开辟一块空间,命名为“s1”,存放的值为常量池中“myString”的内存地址。
通过关键字new定义过程:
String s2=new String("myString");
在程序编译期,编译程序先去字符串常量池检查,是否存在“myString”,如果不存在,则在常量池中开辟一个内存空间存放“myString”;如果存在的话,则不用重新开辟空间,保证常量池中只有一个“myString”常量,节省内存空间。然后在内存堆中开辟一块空间存放new出来的String实例,在栈中开辟一块空间,命名为“s1”,存放的值为堆中String实例的内存地址,这个过程就是将引用s1指向new出来的String实例。
Java的字符串连接符
还有一种就是连接。
String str0 = "a";
String str1 = str0 + "b";
+的处理
让我们看看编译器是怎么处理 + 操作符的。下面是上一个程序片段编译后的字节码指令
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String a
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String b
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: return
}
通过阅读这段字节码,可以发现,我们的+拼接操作实际上被编译器理解成了这个样子:
String str0 = "a";
StringBuilder sb = new StringBuilder();
sb.append(str0).append("b");
String str1 = sb.toString();
实际上是通过StringBuilder来实现的,要注意的是最后还有toString,返回的是一个String对象。
如果是这样:
String str0 = "a";
for (int i = 0; i < 10000; i++) {
str0 += "a";
}
他的字节码:
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String a
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: sipush 10000
9: if_icmpge 38
12: new #3 // class java/lang/StringBuilder
15: dup
16: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
19: aload_1
20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: ldc #2 // String a
25: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore_1
32: iinc 2, 1
35: goto 5
38: return
}
5~35是我们的循环体,在循环体里面,每次拼接都会生成一个StringBuilder的临时对象,那么这个程序片段执行下去就会产生10000个StringBuilder的临时对象,这10000个临时对象都是必要的吗?显然不是,我们可以在循环体外直接创建一个StringBuilder对象,然后在循环体中通过append方法拼接字符串,这样就省下了创建并回收10000个临时对象的消耗。
因此,当我们大量使用字符串拼接的时候,还是使用StringBuilder比较好。
字符串拼接
- 使用字符串连接符拼接 : String s2=”se”+”cond”;
- 使用字符串加引用拼接 : String s12=”first”+s2;
- 使用new String(“”)创建 : String s3 = new String(“three”);
- 使用new String(“”)拼接 : String s4 = new String(“fo”)+”ur”;
- 使用new String(“”)拼接 : String s5 = new String(“fo”)+new String(“ur”);
-
s2 :这个在编译期间就自动进行了优化的,在常量池中存储一个"second",并且s2指向它。
-
s12 : JVM对于字符串引用,由于在字符串的”+”连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即
("first"+s2)
无法被编译器优化,只有在程序运行期来动态分配使用StringBuilder
连接后的新String对象赋给s12。
(编译器创建一个StringBuilder
对象,并调用append()
方法,最后调用toString()
创建新String
对象,以包含修改后的字符串内容),常量池中并没有产生新的字符串常量。 -
s3 : 用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。
但是”three”字符串常量在编译期也会被加入到字符串常量池(如果不存在的话) -
s4 : 同样不能在编译期确定,但是”fo”和”ur”这两个字符串常量也会添加到字符串常量池中,并且在堆中创建String对象。(字符串常量池并不会存放”four”这个字符串)
-
s5 : 原理同s4。