先看下面的例子:
01: public class StringExample
02: {
03: public static void main (String args[])
04: {
05: String s0 = "Programming";
06: String s1 = new String ("Programming");
07: String s2 = "Program" + "ming";
08:
09: System.out.println("s0.equals(s1): " + (s0.equals(s1)));
10: System.out.println("s0.equals(s2): " + (s0.equals(s2)));
11: System.out.println("s0 == s1: " + (s0 == s1));
12: System.out.println("s0 == s2: " + (s0 == s2));
13: }}
这个例子包含了3 个String 型变量,其中两个被赋值以常量表达式“Programming”;另一个被赋值以一个新建的值为“Programming”的String 类的实例。使用equals(...)方法和“= =”运算符进行比较产生了下列结果:
s0。equals(s1): true
s0。equals(s2): true
s0 == s1: false
s0 == s2: true
String.equals()方法比较的是字符串的内容,使用equals(...)方法会对字符串中的所有字符一个接一个地进行比较,如果完全相等那么返回true。 在这种情况下全部字符串都是相同的,所以当字符串s0 与s1 或s2 比较时我们得到的返回值均为true 。运算符“==”比较的是String 实例的引用。在这种情况下很明显s0 和s1 并不是同一个String 实例,但s0 和s2 是同一个。读者也许会问s0 和s2 怎么是同一个对象呢?
这个问题的答案来自于Java语言规范中关于字符串常量String Literals 的章节。本例中“Programming” ,“Program”和“ming”都是字符串常量!!它们在编译期就被确定了当一个字符串由多个字符串常量连接而成时,例如s2 ,它同样在编译期就被确定为一个字符串常量。Java 确保一个字符串常量只有一份拷贝,所以当Programming”和“Program”+“ming”被确定为值相等时,Java 会设置两个变量的引用为同一个常量的引用。在常量池constant pool 中,Java 会跟踪所有的字符串常量。
常量池指的是在编译期被确定,并被保存在已编译的.class 文件中的一些数据。它包含了关于方法,类,接口等等,当然还有字符串常量的信息。当JVM 装载了这个.class 文件。变量s0 和s2 被确定,JVM 执行了一项名为常量池解析constant pool resolution 的操作。该项操作针对字符串的处理过程包括下列3 个步骤,摘自JVM 规范5。4 节::
¹ 如果另一个常量池入口constant pool entry 被标记为CONSTANT_String2 ,并且指出同样的Unicode 字符序列已经被确定,那么这项操作的结果就是为之前的常量
池入口创建的String 实例的引用。
² 否则,如果intern()方法已经被这个常量池描述的一个包含同样Unicode 字符序列的String 实例调用过了,那么这项操作的结果就是那个相同String 实例的引用。
³ 否则,一个新的String 实例会被创建它包含了CONSTANT_String 入口描述的Unicode 字符;序列这个String 实例就是该项操作的结果。
也就是说,当常量池第一次确定一个字符串,在Java 内存栈中就创建一个String 实例。在常量池中,后来的所有针对同样字符串内容的引用,都会得到之前创建的String 实例。当JVM 处理到第6 行时,它创建了字符串常量Programming 的一份拷贝到另一个String 实例中。所以对s0 和s1 的引用的比较结果是false ,因为它们不是同一个对象。这就是为何s0==s1 的操作在某些情况下与s0.equals(s1)不同。s0==s1 比较的是对象引用的值;而s0.equals(s1)实际上执行的是字符串内容的比较。
存在于.class 文件中的常量池,在运行期被JVM 装载,并且可以扩充。此前提到的intern()方法针对String 实例的这个意图提供服务。当针对一个String 实例调用了intern()方法,intern()方法遵守前面概括的第3 步以外的常量池解析规则:因为实例已经存在,而不需要另外创建一个新的。所以已存在的实例的引用被加入到该常量池。来看看另一个例子:
01: import java.io.*;
02:
03: public class StringExample2
04: {
05: public static void main (String args[])
06:{
07: String sFileName = "test.txt";
08: String s0 = readStringFromFile(sFileName);
09: String s1 = readStringFromFile(sFileName);
10:
11: System.out.println("s0 == s1: " + (s0 == s1));
12: System.out.println("s0.equals(s1): " + (s0.equals(s1)));
13:
14: s0.intern();
15: s1.intern();
16:
17: System.out.println("s0 == s1: " + (s0 == s1));
18: System.out.println("s0 == s1.intern(): " +
19: (s0 == s1.intern()));
20: }
21:
22: private static String readStringFromFile (String sFileName)
23: {
24: //…read string from file…
25: }
26: }
这个例子没有设置s0 和s1 的值为字符串常量,取而代之的是在运行期它从一个文件中读取字符串,并把值分配给readStringFromFile(...)方法创建的String 实例。从第9 行开始,程序对两个被新创建为具有同样字符值的String 实例进行处理。当你看到从第11 行到12 行的输出结果时,你会再次注意到这两个对象并不是同一个,但它们的内容是相同的。输出结果如下:
s0 == s1: false
s0.equals(s1): true
s0 == s1: false
s0 == s1.intern(): true
第14 行所做的是将String 实例的引用s0 存入常量池。当第15 行被处理时,对s1.intern()方法的调用,会简单地返回引用s0。 这样一来第17 行和18 行的输出结果正是我们所期望的,s0 与s1 仍旧是截然不同的两个String 。实例因此s0==s1 的结果是false。 而s1.intern()返回的是常量池中的引用值即s0 所,以表达式s0==s1.intern()的结果是true。 假如我们希望将实例s1 存入常量池中,我们必须首先设置s0 为null, 然后请求垃圾回收器garbagecollector 回收被指向s0 的String 实例。在s0 被回收后s1.intern()方法的调用,将会把s1存入常量池。
总的来说在执行等式比较时,应该始终使用String.equals(...)方法,而不是==运算符。如果你还是习惯性地使用==运算符,那么intern()方法可以帮助你得到正确的答案。因为当n 和m 均为String 实例的引用时,语句n.equals(m)与n.intern() ==m.intern()得到的结果是一致的。假如你打算充分利用常量池的优势那么你就应该选择String.intern()方法。