注:本文代码环境为jdk8,其他可能有偏差
先看下面一段所有人都知道答案的代码
String s = "a"+"a";
String s2 = "aa";
System.out.println(s == s2); // true
我们应该思考为什么会输出true,通过反编译可知
jvm直接将上面的"a"+“a"在编译阶段直接变成了"aa”。
继续看下一段代码
String s = "a";
String s1 = s + "a";
String s2 = "aa";
System.out.println(s1 == s2); // false
上面这一段输出false,同样通过反编译
可以看出汇编指令明显比上面的长了许多,然后我们逐个分析s1的产生过程
首先jvm会先生成一个StringBuilder对象然后会添加s和"a",这里我们可以看出第一次添加的时候需要通过ldc出栈解析了字符串s的值,然后添加到StringBuilder对象中。
最后调用StringBuilder对象的toString方法返回一个新的字符串对象,StringBuilder的toString方法如下,所以上面s1==s2为false。
通过上面的分析我们可以知道当String s1 = “a” + "a"时在编译阶段由于可以直接确定s1的值,所以在编译阶段直接将s1的值赋值为aa,而String s1 = s+ "a"在编译阶段中由于不知道s的内容(在编译阶段jvm不会知道一个对象的内容),所以需要运行期间来解析s并且通过StringBuilder来将它们相加。
所以我们在平时写代码的时候对于字符串拼接用StringBuilder来拼接,因为String类型相加底层用的StringBuilder,而每一次String相加都会生成一个对象,使用StringBuilder可以节约内存,避免内存溢出
###String对象存储位置
jdk8中,字符串常量池存放在堆中,所以java堆中分两块,一块是常量池(其中有字符串常量池和其他值类型的引用类型常量池,这里只讨论字符串常量池),另外一块用于存放对象。
众所周知String s = new String(“a”)将会在生成一个String对象,字符串a会不会加入到常量池中呢?我们对String s = new String(“a”)也进行反编译如下
通过对上面进行反编译可以看到使用new创建对象的时候执行了ldc这个指令,ldc指令的意思是操作字符串常量池,如果有直接拉取下来,如果没有就创建一个对象在常量池中。通过反编译我们可以看出使用new String()创建对象的时候我们访问了字符串常量池的,那么是不是创建s对象的时候在常量池也创建了一个"a"呢?
带着这个疑问我们看看String 的构造函数
这个构造函数只是将"a"的value和hash赋值给了新创建的对象,而value是char[]类型的数组,hash也是int类型,这两个都不可能对字符串常量池访问,所以真正的原因只可能是传入的"a"是从字符串常量池中获取的,所以我们在new String()的时候有可能会生成两个对象。所以对于new String(“a”)可能会生成两个对象,一个是字符串类型对象存放在堆中,另外一个就是字符串常量池对象,当然如果a以前在字符串常量池中存在那么将不会创建字符串常量池对象。
验证一下上面的猜想
String s = new String(new char[]{'a','b','c'});
可以看出当使用char[]数组创建对象的时候并没有访问常量池,所以上面的猜想是正确的。通过上面我们可以得出只要在代码中出现"a","ab"这种直接告诉我们这是什么的字符串才会在常量池中创建相应的对象,其他的都是存放在对象堆中,要验证这些可以使用intern()方法。
String.intern()是一个Native方法,它的作用是:如果字符常量池中已经包含一个等于此String对象的字符串,则返回常量池中字符串的引用,否则,将新的字符串放入常量池,并返回新字符串的引用’ ,可以用这个方法来判断字符串是否存在字符串常量池中。
注:进行一个说明,通过上面我们可以了解到String对象存储的两个地方,一个是堆,一个是字符串常量池,堆中的字符串对象底层是通过new String()来创建的,而字符串常量池里面的String object是在jvm通过ClassLoader扫描类中明确指定的字符串将其加入到常量池中,在运行期间只有intern()方法加入进字符串常量池。
下面看几行代码
String s1 = new String("abcd");
String s2 = "ab";
String s3 = "cd";
String s4 = s2 + s3;
//s4通过StringBulider的toString方法创建,所以s4是存放在堆内存中的字符串对象
System.out.println(s4.intern() == s4); // false
System.out.println(s4.intern() == s1.intern()); //true
关于上面结果因为s4是一个新的字符串对象,而abcd在我们刚刚new String(“abcd”)的时候在字符串常量池中创建了该对象,所以s4.intern()得到的是字符串常量池中存放的对象,而s4是存放在对象堆中的对象,所以两者不相等为false,s1.intern()和s4.intern()都是返回的字符串常量池中的对象,所以为true。
关于String 的基础可以观看这里