1. intern()方法简介
如果字符串s在字符串常量池中存在对应字面量,则intern()方法返回该字面量的地址;如果不存在,则创建一个对应的字面量,并返回该字面量的地址
2. String对象与字面量的intern()区别
public static void main(String[] args) {
String s1 = new String("字符串");
String s2 = "字符串";
System.out.println(s2 == s2.intern());
System.out.println(s1 == s1.intern());
System.out.println(s1.intern() == s2.intern());
}
结果是True / False / True,解释如下:
- 对于字符串字面量s2而言,它本身就是字符串常量池中"字符串"常量的引用,因此s2.intern()返回的是字符串常量池中“字符串”常量的地址,与s2本身是相等的,所以为true
- 对于String对象s1而言,它是一个指向堆空间String对象的引用。String对象中保存着一个final byte[]用于存储字符串的value,该成员又指向了字符串常量池中“字符串”这个字面量。因此调用s1.intern(),返回的是字符串常量池中"字符串"字面量的地址。s1本身存的是堆空间String对象的地址,因此二者不相等
- 不管是String对象,还是字面量,只要他们的值相等,调用intern()都会返回同一个字符串常量池的引用,因此s1.intern() == s2.intern()
3. 字符串加法与intern()
public static void test04(){
String s1 = "今天吃" + "炒饭";
String s2 = new String("今天吃") + new String("炒饭");
String s3 = new String("今天吃") + "炒饭";
System.out.println(s1.intern() == s1);
System.out.println(s2 == s2.intern());
System.out.println(s3 == s3.intern());
}
//对应的.class字节码文件
public static void test04() {
String s1 = "今天吃炒饭";
String var10000 = new String("今天吃");
String s2 = var10000 + new String("炒饭");
var10000 = new String("今天吃");
String s3 = var10000 + "炒饭";
System.out.println(s1.intern() == s1);
System.out.println(s2 == s2.intern());
System.out.println(s3 == s3.intern());
}
返回结果为true / false / false,解析如下:
如果字符串加法中全是字面量相加,则在前端编译为.class字节码文件时,会被直接优化为加和,例如s1在.class文件中被优化为:s1 = "今天吃炒饭",因此s1在被后端编译时,编译器/解释器是看不到相加的过程的,它等效于直接对s1以字面量赋值,所以s1 == s1.intern()
如果字符串加法中存在String对象,前端编译不会将其优化为字面量。关于这个过程,有视频/博客说用的是StringBuilder来做的相加,即new StringBuilder("今天吃").append("炒饭").toString()完成的调用。但是查看字节码文件后,发现新版的JDK不是使用此方式完成的。JDK15中,字符串相加的字节码指令为:
invokedynamic #61 <makeConcatWithConstants, BootstrapMethods #0>
它调用了一个makeConcatWithConstants的方法,此方法位于StringConcatFactory类中,JDK对该方法的描述为:
This strategy replicates what StringBuilders are doing: it builds the
* byte[] array on its own and passes that byte[] array to String
* constructor. This strategy requires access to some private APIs in JDK,
* most notably, the private String constructor that accepts byte[] arrays
* without copying.
即底层是:拼接得到一个byte[]数组,然后传递给一个String的构造函数得到拼接结果。重点在于其构造函数的参数是byte[],而不是String。因此单独看下方这一行代码,是不会出现"今天吃炒饭"这个字符串的,故字符串常量池中不会出现"今天吃炒饭"这个字面量。则调用s2.intern()方法时,会将s2的地址拷贝一份放到常量池中,然后返回s2的地址,故s2 == s2.intern()。
String s2 = new String("今天吃") + new String("炒饭");
但是这跟我们的结果不一样,结果中得到的是false,这个一个小坑。因为s1中我们令s1 = "今天吃" + "炒饭",已经拼出了一个"今天吃炒饭"字面量,这才导致s1.intern()已经在常量池中得到了字面量。如果我们去掉第一行:
public static void test04(){
String s2 = new String("今天吃") + new String("炒饭");
System.out.println(s2 == s2.intern());
}
得到的结果就是true
4. StringBuilder与intern()
StringBuilder中维护着一个可变的byte[]用于存储数据,其append(String s)方法是为byte[]数组添加上一些数据,并更新byte[]的有效长度。因此在append过程中,是不会有任何新的字符串被产生的。仅当StringBuilder对象调用toString方法时,才会返回一个new出来的字符串。因此append()过程中,不会有任何中间值出现,例如String s1 = "a".append("b").append("c"),是不会出现"ab"这个字符串的,因为只有调用toString()方法后,才会真正得到字符串对象,其余都是byte[]数组
因此考察如下代码:
public void test01(){
String s1 = new StringBuffer("计算机").append("软件").toString();
System.out.println(s1==s1.intern());
System.out.println("计算机软件" == s1);
}
打印true / true,解析如下:
- 首先出现了"计算机"和"软件"两个字符串常量,常量池中肯定存在二者。new StringBuffer .append()方法调用后,byte[]数组中存储着"计算机软件"的byte[]形式,此时字符串常量池中没有"计算机软件"这个字面量。调用toString()方法后,用byte[]作为参数传入构造器,也没有出现"计算机软件"这个字面量。即第一行代码执行结束后,s1中存储的是堆空间String的地址,且常量池中没有“计算机软件”这个字面量。
- 当调用s1.intern()后,由于字符串常量池和s1对象都在堆空间中,为了节省空间,不会再将"计算机软件"这个字面量copy一份到常量池中,而是将s1的地址拷贝一份,作为intern()方法的返回值。因此s1 == s1.intern()。此后再使用"计算机软件"这个字面量时,自动返回常量池中的地址,即s1的值,因此"计算机软件" == s1也为true
而对于以下代码:
String s1 = new StringBuilder("字符串").toString();
System.out.println(s1.intern() == s1);
System.out.println(s1 == "字符串".intern());
得到的结果为false / false,原因如下:
在new StringBuilder的过程中,使用到了"字符串"这个字面量,因此new StringBuilder("字符串")已经在常量池中创建了"字符串"字面量。调用toString方法返回堆空间的地址。由于"字符串"字面量已经存在于常量池中,故s1.intern()返回其地址;而s1为堆空间中String对象的地址,二者显然不同。第二个判断也同理