最近在复习Java的时候遇到关于String类型的一些疑惑,查阅了一些资料后算是有一点点心得,记于此。
一、看如下代码:
String a = "programming"; String b = new String("programming"); String c = "program" + "ming"; System.out.println(a == b); System.out.println(a == c); System.out.println(a.equals(b)); System.out.println(a.equals(c)); System.out.println(a.intern() == c.intern());
上面程序的输出会是什么呢?答案是:
false true true true true
对于“programming”这种字面常量,它的值在编译期就能过确定下来,因此也被称为编译期常量,编译期常量存储在常量池中。Java虚拟机会确保编译期常量的对象只有一个。而对于字面值类型的字符串常量(也就是“programming”这样的字符串),使用字符串连接符“+”对多个字面值类型的字符串常量进行连接的时候,编译期在编译时会对其进行优化,不需要等到运行期再去确定他们的值,直接将连接符去掉,把它们合并成一个字面值常量,并确保字面值常量的对象在常量池中只有一个。因此上面的代码中字符串a与字符串c等价。而对于字符串变量b,因为是使用new操作符生成的对象,因此它指向堆中的一个对象,这个对象与常量池中的“programming”自然不是同一个对象。
对于最后一个输出语句,调用了String类的intern方法。我们来看一下JavaAPI中的介绍:
public String intern() 返回字符串对象的规范化表示形式。一个初始为空的字符串池,它由类 String 私有地维护。 当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此String 对象的引用。 它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。 所有字面值字符串和字符串赋值常量表达式都使用 intern 方法进行操作。
所以变量a和变量c调用intern方法返回的对象时一样的。
好的,看完上面的解释,再来看一句代码,看能不能正确理解:
String s = "a" + "b" + "c" + "d";问,执行上面的代码以后,会创建多少个String类型的对象呢?答案是只会创建1个。想想为什么。
二、看如下代码:
String a = "HelloWorld"; final String b = "Hello"; String c = "Hello"; String d = "World"; String e = "Hello" + "World"; String cd = c + d; String f = b + d; String g = b + "World"; String h = c + "World"; System.out.println(a == e); System.out.println(a == cd); System.out.println(a == f); System.out.println(a == g); System.out.println(a == h);上面的程序又会输出什么呢?答案是:
true false false true false
对于代码12行的结果我们上面已经解释了。可为什么下面几行输出都这么奇怪?这里我们必须要区分开字面值常量字符串和字符串变量在字符串运算中的区别。 我们来看看上面代码的字节码文件,了解一下JVM到底做了啥。
String HelloWorld //对应上面代码的第一行 String Hello //第二行 String Hello //第三行 String World //第四行 String HelloWorld //五 //上面是几个字符串变量的复制,还记得上面API中说的吗? //“所有字面值字符串和字符串赋值常量表达式都使用 intern 方法进行操作” //所以以上的字符串变量直接引用了常量池中的对象。 class java/lang/StringBuilder //创建一个StringBuilder 对象。 Method java/lang/StringBuilder."<init>":()V //将字符串变量c、d添加到StringBuilder 中。 Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; Method java/lang/StringBuilder.toString:()Ljava/lang/String; //上面这一块对应String cd = c + d;这一句代码。 class java/lang/StringBuilder //下面这一块对应 String f = b + d; Method java/lang/StringBuilder."<init>":()V String Hello //因为b是final类型,也就是常量,所以直接取常量池中的对象。 //虽然是字面值常量,但是它跟字符串变量d做连接操作,所以也要放进StringBuilder对象中。 Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; Method java/lang/StringBuilder.toString:()Ljava/lang/String; String HelloWorld //这一句对应String g = b + "World"; //因为b是常量,“World”是字面值常量,所以直接去掉“+”并将其连接。 //下面的对应最后一句String h = c + "World"; //同样是生成一个StringBuilder,然后将一个变量和一个字面值常量添加进StringBuilder中。 class java/lang/StringBuilder Method java/lang/StringBuilder."<init>":()V Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; String World Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; Method java/lang/StringBuilder.toString:()Ljava/lang/String; 由上面的分析基本就可以明白JVM做了什么,也就明白了为什么代码会输出上面的结果。 首先分清楚两个概念: 字符串变量 (String a)和 字面值常量 (“HelloWorld”)。 简单说,对于只有 字面值常量的连接操作 ,JVM会将他们直接连接在一起并将其放入到常量池,作为 唯一 的常量对象。要注意, 被final修饰的也是常量哟 。比如上面的final String b = “Hello”,常量b也要当做字面值常量来看待。因为字面值常量在编译期间就可以确定其值,所以编译期将会优化代码,也就是不需要等到运行期间,直接得到他们的值并放到常量池中。所以上面的a、e以及g三者是相同的,都是引用了常量池中的同一个唯一的对象。 而对于字符串变量进行 连接操作 (注意是连接操作),如String cd = c + d或者String h = c + “World”, JVM采取的措施是先生成一个 StringBuilder对象 ,然后将需要连接的两者都添加到StringBuilder对象中。操作大体如下:StringBuilder builder = new StringBuilder(); builder.append(c); builder.append(d); cd = builder.toString(); 我们再来看StringBuilder 类的toString()方法。
public String toString() { // Create a copy, don't share the array return new String(value, 0, count); } 看到了吧, 新创建了一个String对象 。所以说,已经跟字面值常量对象不是同一个了。