我们都知道,在Java中字符串可以用+连接,也可以使用StringBuilder或StringBuffer连接。
String str = "abc"+"xyz";
那么这几种方式由什么区别呢。当然你可能会知道以下几点
- String是只读字符串,String引用的字符串内容是不能被改变的
- StringBuffer/StringBuilder 表示的字符串对象可以直接进行修改
- StringBuffer是线程安全的,他的方法都被synchronized修饰过,StringBuilder 是线程不安全的,通常效率要比StringBuffer要高一点
但是现在需要对String的+进行深层次的探索。
下面一段代码
public class Main {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "xxx" + s1 + "zzz" +1;
System.out.println(s2);
}
}
从表面上看,对字符串和整型使用"+"号并没有什么区别,但实际上看看这段代码的本质,你就会发现其中奥秘。
使用反编译工具jad对代码进行反编译, 这里分享一个jad百度云下载地址https://pan.baidu.com/s/1_QDofa8t5thVAiPc5C5mzg 提取码: 9fn3
jad -o -a -s java Main.class
其中 -o覆盖输出文件无需确认,-a生成JVM指令作为注释,-s 输出文件后缀名(默认是.jad)。这里生成JVM指令作为参考。
import java.io.PrintStream;
public class Main
{
public Main()
{
// 0 0:aload_0
// 1 1:invokespecial #1 <Method void Object()>
// 2 4:return
}
public static void main(String args[])
{
String s1 = "abc";
// 0 0:ldc1 #2 <String "abc">
// 1 2:astore_1
String s2 = (new StringBuilder()).append("xxx").append(s1).append("zzz").append(1).toString();
// 2 3:new #3 <Class StringBuilder>
// 3 6:dup
// 4 7:invokespecial #4 <Method void StringBuilder()>
// 5 10:ldc1 #5 <String "xxx">
// 6 12:invokevirtual #6 <Method StringBuilder StringBuilder.append(String)>
// 7 15:aload_1
// 8 16:invokevirtual #6 <Method StringBuilder StringBuilder.append(String)>
// 9 19:ldc1 #7 <String "zzz">
// 10 21:invokevirtual #6 <Method StringBuilder StringBuilder.append(String)>
// 11 24:iconst_1
// 12 25:invokevirtual #8 <Method StringBuilder StringBuilder.append(int)>
// 13 28:invokevirtual #9 <Method String StringBuilder.toString()>
// 14 31:astore_2
System.out.println(s2);
// 15 32:getstatic #10 <Field PrintStream System.out>
// 16 35:aload_2
// 17 36:invokevirtual #11 <Method void PrintStream.println(String)>
// 18 39:return
}
}
从反编译的代码中可以看出,String的+拼接,实际上是StringBuilder拼接然后转为String的,因此,我们可以得出结论,在 Java 中无论使用何种方式进行字符串连接,实际上都使用的是 StringBuilder ,那这样是不是就表示String的+拼接和StringBuilder的效果是一样的呢?从运行结果上看,两者是等效的。但是从效率和资源消耗上看,两者区别很大。当使用简单字符串相加使,没有太大区别,但是在循环字符串中,这两者的差距就很大。
看下面一段代码,在循环中使用+拼接字符串
public class Main2 {
public static void main(String[] args) {
String s = "";
for (int i = 0; i < 10; i++) {
s = s + i + " ";
}
System.out.println(s);
}
}
使用jad.exe -o -s java .\Main2.class
反编译,这里不生成JVM指令.
import java.io.PrintStream;
public class Main2
{
public Main2()
{
}
public static void main(String args[])
{
String s = "";
for(int i = 0; i < 10; i++)
s = (new StringBuilder()).append(s).append(i).append(" ").toString();
System.out.println(s);
}
}
可以看出在循环中每次都创建了一个新的StringBuilder对象,占用大量资源。对此我们将其改进一下,使用StringBuilder连接
public class Main3 {
public static void main(String[] args) {
StringBuilder s = new StringBuilder();
for (int i = 0; i < 10; i++) {
s.append(i);
s.append(" ");
}
System.out.println(s);
}
}
生成的字节码
import java.io.PrintStream;
public class Main3
{
public Main3()
{
}
public static void main(String args[])
{
StringBuilder s = new StringBuilder();
for(int i = 0; i < 10; i++)
{
s.append(i);
s.append(" ");
}
System.out.println(s);
}
}
可以看出源码和字节码没有区别,也没有生成额外的对象。
但是要注意,使用StringBuilder拼接字符串时,不要和+混用,否则还会生成更多对象
例如
public class Main4 {
public static void main(String[] args) {
StringBuilder s = new StringBuilder();
for (int i = 0; i < 10; i++) {
s.append(i + " ");
}
System.out.println(s);
}
}
反编译源码
import java.io.PrintStream;
public class Main4
{
public Main4()
{
}
public static void main(String args[])
{
StringBuilder s = new StringBuilder();
for(int i = 0; i < 10; i++)
s.append((new StringBuilder()).append(i).append(" ").toString());
System.out.println(s);
}
}
可以看出Java会将+连接的字符串通过StringBuilder对象连接,这样还是生成了不必要的对象,造成资源浪费,在IDEA中,如果是使用这种方式拼接字符串,它会提出警告.
通过以上示例,我们明白String通过+拼接字符串的本质,拼接字符串时尽量使用StringBuilder,并且二者不要混用。
以下是一个判断字符串相等的示例,让我们从反编译的角度看原因
public class StringEqualTest {
public static void main(String[] args) {
String s1 = "Programming";
String s2 = new String("Programming");
String s3 = "Program";
String s4 = "ming";
String s5 = "Program" + "ming";
String s6 = s3 + s4;
System.out.println(s1 == s2); //false
System.out.println(s1 == s5); //true
System.out.println(s1 == s6); //false
System.out.println(s2 == s6); //false
System.out.println(s1 == s2.intern()); //true
System.out.println(s1 == s6.intern()); //true
System.out.println(s2 == s2.intern()); //false
}
}
我们都知道Java中有个常量池,并且符合以下条件
- 由操作符 new 调用的 String的构造器产生的对象,如 String s = new String(“1”), JVM 会先使用常量池来管理 ,再调用String 类的构造器来创建一个新的 String 对象,新创建的 String 对象被保存在堆内存中 。
- 字符串常量初始化的对象 (包括在编译时就可以计算出来的字符串值) , 如, String s =“1”; 存到常量池中。
- 堆中字符串相加的表达式,如:String s3 = new String(“1”) + new String(“1”);, 其结果 s3 仍存到堆中。
- 字符串相加的表达式中,若包含字符串常量的算子,其结果仍存到常量池中。
- String对象的intern()方法会得到字符串对象在常量池中对应的版本的引用,如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;
对代码进行反编译
import java.io.PrintStream;
public class StringEqualTest
{
public StringEqualTest()
{
}
public static void main(String args[])
{
String s1 = "Programming";
String s2 = new String("Programming");
String s3 = "Program";
String s4 = "ming";
String s5 = "Programming";
String s6 = (new StringBuilder()).append(s3).append(s4).toString();
System.out.println(s1 == s2);
System.out.println(s1 == s5);
System.out.println(s1 == s6);
System.out.println(s2 == s6);
System.out.println(s1 == s2.intern());
System.out.println(s1 == s6.intern());
System.out.println(s2 == s2.intern());
}
}
- s1和s2一个在常量池,一个在堆中,所以是false
- s5由两个常量池中数据相加,反编译后可以看到,值已经运算出来了,其结果仍保存在常量池中,所以s1==s5
- s6是两个字符串相加,通过StringBuilder的append连接而成,所以s1肯定不等于s6
- s2和s6属于两个对象,肯定不相等
- s2退回常量池后,返回常量池的引用,所以和s1相等
- s6退回常量池后,返回常量池的引用,所以和s1相等
- 一个在堆中,一个在常量池中,不相等
在未了解String的+运算符实质之前,对于s1==s6的判断,我也是稍有疑惑,但是在反编译了解其运行原理后就可以很清楚的了解s5,s6的区别。