字符串拼接原理
- 1、常量与常量的拼接结果在常量池,原理是编译器优化(常量可以指final修饰的变量、字面量,例如final String s1 = “abc”,字面量指“abc”)
- 2、常量池中不会存在相同内容的常量。
- 3、只要其中有一个是变量,结果就在堆中。变量的拼接的原理是StringBuilder
- 4、如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
intern()
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.
It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.
个人翻译:
当intern()方法被调用的时候,如果字符串常量池中含有一个字符串,与调用这个方法的this的字符串对象相同,然后这个常量池中字符串将会被返回。否则,这个字符串对象将会被添加到常量池中并且返回这个字符串对象的引用。
总结String的intern()的使用:
- jdk1.6中,将这个字符串对象尝试放入串池
- 如果池中有,返回已有对象地址
- 如果没有,把对象复制一份,放入串池,并返回对象地址
- jdk1.7及以后,将这个字符串对象尝试放入串池
- 如果池中有,返回已有对象地址
- 没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址
案例1
public void test02(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
/*
如下s1 + s2的细节
StringBuilder sb = new StringBuilder();
sb.append("a");
sb.append("b");
sb.toSting(); --> 与等于new String("ab");
*/
// 一个是对象的地址,一个是常量池中的地址,索引false
System.out.println(s3 == s4);//false
System.out.println(s3.equals(s4));//true
}
equals为true可以看String的方法equals()底层代码
public boolean equals(Object anObject) {
if (this == anObject) { // 判断地址是否相同,如果相同既同一个东西,返回true
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;//获取this.value的长度
if (n == anotherString.value.length) {//长度相同,继续比下去,不同则肯定不一样,返回false,下面的操作就是继续比较各个char字符相不相同,不同则返回false,全部相同则返回true
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
观察jclasslib
局部变量表
指令
0 ldc #5 <a>
2 astore_1
3 ldc #6 <b>
5 astore_2
6 ldc #7 <ab>
8 astore_3
9 new #8 <java/lang/StringBuilder>
12 dup
13 invokespecial #9 <java/lang/StringBuilder.<init> : ()V>
16 aload_1
17 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
20 aload_2
21 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
24 invokevirtual #11 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
27 astore 4
29 getstatic #12 <java/lang/System.out : Ljava/io/PrintStream;>
32 aload_3
33 aload 4
35 if_acmpne 42 (+7)
38 iconst_1
39 goto 43 (+4)
42 iconst_0
43 invokevirtual #13 <java/io/PrintStream.println : (Z)V>
46 getstatic #12 <java/lang/System.out : Ljava/io/PrintStream;>
49 aload_3
50 aload 4
52 invokevirtual #14 <java/lang/String.equals : (Ljava/lang/Object;)Z>
55 invokevirtual #13 <java/io/PrintStream.println : (Z)V>
58 return
序号 | 指令 | 说明 |
---|---|---|
0 | ldc #5 | |
2 | astore_1 | 将a赋值给本地变量表为1的索引,既s1 |
3 | ldc #6 | |
5 | astore_2 | 将b赋值给本地变量表为2的索引,既s2 |
6 | ldc #7 | |
8 | astore_3 | 将ab赋值给本地变量表为3的索引,既s3 |
9 | new #8 <java/lang/StringBuilder> | 创建StringBuilder()对象 |
12 | dup | |
13 | invokespecial #9 <java/lang/StringBuilder. : ()V> | 初始化StringBuilder() |
16 | aload_1 | 取出索引为1的内容,既取出s1中的内容“a” |
17 | invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> | 将取出的内容加入到StringBuilder()对象中,前几步类似于StringBuilder sb = new StringBuilder();sb.append(“a”); |
20 | aload_2 | 取出索引为2的内容,既取出s2中的内容“b” |
21 | invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> | |
24 | invokevirtual #11 <java/lang/StringBuilder.toString : ()Ljava/lang/String;> | 执行sb.toString(); |
27 | astore 4 | 将StringBuilder的toString赋值给本地变量表为4的索引,既s4 |
29 | getstatic #12 <java/lang/System.out : Ljava/io/PrintStream;> | |
32 | aload_3 | |
33 | aload 4 | |
35 | if_acmpne 42 (+7) | |
38 | iconst_1 | |
39 | goto 43 (+4) | |
42 | iconst_0 | |
43 | invokevirtual #13 <java/io/PrintStream.println : (Z)V> | |
46 | getstatic #12 <java/lang/System.out : Ljava/io/PrintStream;> | |
49 | aload_3 | |
50 | aload 4 | |
52 | invokevirtual #14 <java/lang/String.equals : (Ljava/lang/Object;)Z> | |
55 | invokevirtual #13 <java/io/PrintStream.println : (Z)V> | |
58 | return |
案例2
public void test01(){
final String s1 = "a";//常量引用
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);//true
}
观察jclasslib
局部变量表
指令
0 ldc #5 <a>
2 astore_1
3 ldc #6 <b>
5 astore_2
6 ldc #7 <ab>
8 astore_3
9 ldc #7 <ab>
11 astore 4
13 getstatic #8 <java/lang/System.out : Ljava/io/PrintStream;>
16 aload_3
17 aload 4
19 if_acmpne 26 (+7)
22 iconst_1
23 goto 27 (+4)
26 iconst_0
27 invokevirtual #9 <java/io/PrintStream.println : (Z)V>
30 getstatic #8 <java/lang/System.out : Ljava/io/PrintStream;>
33 aload_3
34 aload 4
36 invokevirtual #10 <java/lang/String.equals : (Ljava/lang/Object;)Z>
39 invokevirtual #9 <java/io/PrintStream.println : (Z)V>
42 return
序号 | 指令 | 说明 |
---|---|---|
0 | ldc #5 | |
2 | astore_1 | |
3 | ldc #6 | |
5 | astore_2 | |
6 | ldc #7 | |
8 | astore_3 | |
9 | ldc #7 | |
11 | astore 4 | 观察发现s4的赋值方式是直接将ab在常量池的地址返回给s4 |
13 | getstatic #8 <java/lang/System.out : Ljava/io/PrintStream;> | |
16 | aload_3 | |
17 | aload 4 | |
19 | if_acmpne 26 (+7) | |
22 | iconst_1 | |
23 | goto 27 (+4) | |
26 | iconst_0 | |
27 | invokevirtual #9 <java/io/PrintStream.println : (Z)V> | |
30 | getstatic #8 <java/lang/System.out : Ljava/io/PrintStream;> | |
33 | aload_3 | |
34 | aload 4 | |
36 | invokevirtual #10 <java/lang/String.equals : (Ljava/lang/Object;)Z> | |
39 | invokevirtual #9 <java/io/PrintStream.println : (Z)V> | |
42 | return |
练习
public void test01(){
String s1 = "javaEEHotSpot";
// 第一种情况
String s2 = "javaEE";
String s3 = s2 + "HotSpot";
System.out.print(s1 == s3);
// 第二种情况
final String s4 = "javaEE";
String s5 = s4 + "Hotspot";
System.out.print(s1 == s5);
}
答案是false true
问题
问题1:new String(“ab”)会创建几个对象
public static void main(String[] args) {
String ab = new String("ab");
}
观察字节码指令(jclasslib)
0 new #2 <java/lang/String>
3 dup
4 ldc #3 <ab>
6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
9 astore_1
10 return
会创建两个对象,一个是new关键字在堆空间创建,另一个是在常量池中有"ab"
问题2:new String(“a”) + new String(“b”)会创建几个对象
public static void main(String[] args) {
String ab = new String("a") + new String("b");
}
观察字节码指令(jclasslib)
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <a>
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <b>
25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
34 astore_1
35 return
- 对象1:new StringBuilder()
- 对象2:new String(“a”);
- 对象3:常量池中的a
- 对象4:new String(“b”)
- 对象5:常量池中的b
问题1和问题2对比
在问题2中并没有在常量池中创建”ab“对象,(toString不会在常量池中生成)
小结练习
public static void main(String[] args) {
/**
* 执行顺序
* new String(),用s接收保存地址,常量池中存储"1",初始化String,将"1"放入,并"1"在常量池中返回地址
* 判断常量池中是否含有"1",没有则创建并返回引用,有则直接返回引用
* 将常量池中"1"的引用返回,用说s2接收保存地址
* 一个是保存字符串常量池的地址,一个是保存堆空间中对象的地址
*/
String s = new String("1");
s.intern();//调用此方法前,常量池中已经含有1,调用方法时不会再去常量池中创建
String s2 = "1";
System.out.println(s == s2);//jdk6:false jdk7:false
/**
* s3记录的引用为new String("11")的引用,这个不会在常量池中创建"11"
* jdk6中,常量池发现没有"11",则创建"11"对象(这个时候常量池在永久代)。在jdk7中,常量池发现没有"11",则创建"11"对象,但是保存的是上面的new String("11")的地址引用(jdk7中的常量池是放在堆内存空间,因此不会再去创建"11")
* s4保存的是常量池中”11“的地址,jdk返回的是常量池中的”11“,jdk7返回的是堆内存中的new String("11")的地址
*/
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);//jdk6:false jdk7/8:true
}
结论
- equals比较的是this的字符串值和要比较的值的字符串相不相同
- 而等等号比较的是对象的地址一不一样
- 如果拼接符号左右都是常量或者常量引用(final修饰的变量),则仍然使用编译器优化,既非new StringBuilder()的方式
- 针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final的时候建议使用上。