下面程序的运行结果是什么?
String stra = "ABC";
String strb = new String("ABC");
System.out.println(stra == strb); //1,false
System.out.println(stra.equals(strb)); //2,true
对于 1 和 2 中两个都是显式创建的新对象,使用 == 总是不等,String 的 equals 方法有被重写为值判断,所以 equals 是相等的。
String str1 = "123";
System.out.println("123" == str1.substring(0)); //3,true
System.out.println("23" == str1.substring(1)); //4,false
对于 3 和 4 中 str1 的 substring 方法实现里面有个 index == 0 的判断,当 index 等于 0 就直接返回当前对象,否则新 new 一个 sub 的对象返回,而 == 又是地址比较,所以结果如注释。源码如下:
String str5 = "NPM";
String str6 = "npm".toUpperCase();
System.out.println(str5 == str6); //7,false
System.out.println(str5.equals(str6)); //8,true
toUpperCase 方法内部创建了新字符串对象,可以简单看下源码:
public String toUpperCase(Locale var1) {
...
return new String(var15, 0, var3 + var4);
}
String str9 = "a1";
String str10 = "a" + 1;
System.out.println(str9 == str10); //11,true
对于 11 来说当两个字符串常量连接时(相加)得到的新字符串依然是字符串常量且保存在常量池中只有一份。
String str11 = "ab";
String str12 = "b";
String str13 = "a" + str12;
System.out.println(str11 == str13); //12,false
对于 12 来说当字符串常量与 String 类型变量连接时得到的新字符串不再保存在常量池中,而是在堆中新建一个 String 对象来存放,很明显常量池中要求的存放的是常量,有 String 类型变量当然不能存在常量池中了。
String str14 = "ab";
final String str15 = "b";
String str16 = "a" + str15;
System.out.println(str14 == str16); //13,true
对于 13 来说此处是字符串常量与 String 类型常量连接,得到的新字符串依然保存在常量池中,因为对 final 变量的访问在编译期间都会直接被替代为真实的值。
private static String getBB() {
return "b";
}
String str17 = "ab";
final String str18 = getBB();
String str19 = "a" + str18;
System.out.println(str17 == str19); //14,false
对于 14 来说 final String str18 = getBB() 其实与 final String str18 = new String(“b”) 是一样的,也就是说 return “b” 会在堆中创建一个 String 对象保存 ”b”,虽然 str18 被定义成了 final,但不代表是常量,因为虽然将 str18 用 final 修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此指向的不是同一个对象,所以可见看见并非定义为 final 的就保存在常量池中,很明显此处 str18 常量引用的 String 对象保存在堆中,因为 getBB() 得到的 String 已经保存在堆中了,final 的 String 引用并不会改变 String 已经保存在堆中这个事实;对于 str18 换成 final String str18 = new String("b"); 一样会返回 false,原因同理。
String str20 = "ab";
String str21 = "a";
String str22 = "b";
String str23 = str21 + str22;
System.out.println(str23 == str20); //15,false
System.out.println(str23.intern() == str20); //16,true
System.out.println(str23 == str20.intern()); //17,false
System.out.println(str23.intern() == str20.intern()); //18,true
对于 15 到 18 来说 str23 == str20 就是上面刚刚分析的,而对于调用 intern 方法如果字符串常量池中已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定)则返回字符串常量池中的字符串,否则将此 String 对象添加到字符串常量池中,并返回此 String 对象的引用,所以 str23.intern() == str20 实质是常量比较返回 true,str23 == str20.intern() 中 str23 就是上面说的堆中新对象,相当于一个新对象和一个常量比较,所以返回 false,str23.intern() == str20.intern() 就没啥说的了,指定相等。
注释 11 到 14 深刻的说明了我们在代码中使用 String 时应该留意的优化技巧!特别说明 String 的 + 和 += 在编译后实质被自动优化为了 StringBuilder 和 append 调用,其实在Java中是先构建一个StringBuiler对象,然后使用append()方法拼接字符串最后调用toString()方法生成字符串。但是如果在循环等情况下调用 + 或者 += 就是在不停的 new StringBuilder 对象 append 了,这是及其浪费的,应该直接创建StringBuilder来实现。
通过这些题说明要想玩明白 Java String 对象的核心其实就是玩明白字符串的堆栈和常量池,虚拟机为每个被装载的类型维护一个常量池,常量池就是该类型所用常量的一个有序集合,包括直接常量(String、Integer 和 Floating Point 常量)和对其他类型、字段和方法的符号引用,池中的数据项就像数组一样是通过索引访问的,由于常量池存储了相应类型所用到的所有类型、字段和方法的符号引用,所以它在 Java 程序的动态链接中起着核心的作用。