String 在Java中使用的非常多,只要我们写代码可能就会用到它,但我们真的了解它吗?
一、字符串的创建
1.1、使用双引号申明
在声明字符串时,直接使用双引号声明出来的String对象会直接存储在常量池中。
String str1="hello";
String str2="hello";
System.out.println(str1==str2);//true
在使用双引号申明"hello"时,jvm会检查常量池中是否有该对象,有就返回它的引用,没有就创建一个将其添加到常量池中,再返回它的引用。因此str1==tr2结果为true。
1.2使用new关键字创建String对象
看看下面这行代码:
String str1=new String("hello");
经常会遇到有人问上面这行代码创建了几个对象这种问题,事实上我写这篇博客的起因就是它,原谅我的无知。
上面那行代码总共创建了两个对象,一个是常量池中的“hello”,一个是通过new创建的保存在堆上的String对象。
看下它的字节码就很清楚了。
0: new #2 // class java/lang/String 创建String对象,但还没调用它的<init>方法
3: dup
4: ldc #3 // String hello 从常量池中加载"hello"对象到操作数栈顶
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 调用构造方法,并将"hello"传进去。
9: astore_1
10: return
在类被加载时"hello"将会在常量池被创建,接着再调用new指令,JVM会在堆中创建一个String对象,并调用器构造函数,引用常量池中的"hello"字符串
再来看看下面的new String(“hello”)创建了几个对象
String str1="hello";
String str2=new String("hello");
此时new String(“hello”)实际上只创建了一个。因为在第一行hello"会在常量池被创建,第二行因为是new指令,JVM会在堆中创建一个String对象,但是它引用了常量池已经存在的"hello"
二、字符串拼接
字符串拼接可以分为分多钟情况。
2.1、字面量之间的+
String str1="hello"+"world";
字节码如下:
可以看到对于这种字面量之间的相加,在编译时会被拼接起来。从ldc指令可以看出,它是将拼接后的"helloworld"加载到操作数栈顶,这种字面量的相加只会产生一个对象。
现在我们知道了字面量相加的本质来看一个问题。
String str1="hello";
String str2="he"+"llo";
System.out.println(str1==str2);
str1和str2相等吗,答案是相等的,他们指向的是常量池中的同一个string对象。首先会在常量池中查看有没有"hello",第一次肯定没有,没有就创建,然后将它的引用返回给str1,对于"he"+“llo"在编译时会被拼接为"hello”,然后jvm会取常量池看有没有,因为"hello"已经存在了,就直接返回它的引用,所以str1==str2成立。
2.2、变量之间的+
String str1="hello";
String str2="world";
String str3=str1+str2;
这3行代码一共会产生三个对象,其中两个是常量池中的"hello"和"world",还有一个是在堆中new出来的StringBuild对象。
字节码如下:
0: ldc #2 // String hello
2: astore_1
3: ldc #3 // String world
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3
25: return
简单的解释下:在字节码的第0行ldc指令,会将常量池中的"hello"加载到操作数栈顶,然后第2行astore_1指令会将操作数栈顶元素也就是"hello"保存到本地变量表1的位置,然后在第3行又将常量池中的"world"加载到操作数栈顶,在第5行将栈顶元素"world"保存到本地变量表2的位置。在第6行会通过new创建一个StringBuilder对象,在第13行执行aload_1指令将本地变量表1位置保存的"hello"加载到操作数栈顶,然后调用StringBuilder的append方法将"hello"传进去,第17,18行的指令调用了StringBuilder的append方法将“world”传进去。从字节码可以看出,这种字符串变量之间的相加实际上会调用StringBuilder的append方法将所有字符串添加进来。
扩展
很多人会告诉我们不要在循环中做字符串的相加,是因为在循环的过程中每循环一次就会创建一个StringBuilder对象。
String str="";
for (int i=0;i<10;i++){
str=str+String.valueOf(i);
}
System.out.println(str);
字节码
stack=2, locals=3, args_size=1
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: bipush 10
8: if_icmpge 39
11: new #3 // class java/lang/StringBuilder
14: dup
15: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
18: aload_1
19: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: iload_2
23: invokestatic #6 // Method java/lang/String.valueOf:(I)Ljava/lang/String;
26: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: astore_1
33: iinc 2, 1
36: goto 5
39: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
42: aload_1
43: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
46: return
这里主要看下第8行的字节码指令if_icmpge,它会比较栈顶两个int型整数的大小(i和10),不符合就跳转到39行。符合条件就执行下面的语句,然后再第33行iinc指令将变量i+1,然后再执行goto指令跳转到第5行将本地变量表的i加载到操作数栈顶,第6行bipush将整数10加载到操作数栈顶,在第8行if_icmpge又进行比较判断(i和10)。通过这种方式就实现了循环,而new指令是在循环中的,因此每次循环都会创建一个StringBuilder对象。
代码类似下面:
String str = "";
for(int i = 0; i < 10; i++) {
str = (new StringBuilder(str)).append(String.valueOf(i)).toString();
}
System.out.println(str);
在循环中做字符串的相加会导致在短时间创建大量的临时对象。因此可以像下面这样写。
StringBuilder builder = new StringBuilder();
for(int i = 0; i < 10; i++) {
builder.append(i);
}
System.out.println(builder.toString());
2.3、常量变量之间的+
final String str1="hello";
final String str2="world";
String str3=str1+str2;
这3行代码和2.2的区别在于str1和str2是用final修饰的,此时String str3=str1+str2;这行代码就等同于String str3=“helloworld”。这种final修饰的字符串相加时会出现编译时替换,可以将它看成字面量之间的相加。
因此下面这个问题就很好回答了。
final String str1="hello";
final String str2="world";
String str3=str1+str2;
String str4="helloworld";
System.out.println(str3==str4);//true
2.3、变量和字面量之间的+
String str1="hello";
String str2=str1+"world";
对于这种会创建一个StringBuilder对象,然后两次调用它的append方法将常量池中的hello和world添加进去。