String类也许是Java Coders最早接触的类之一。我不算资深程序员,但也有好几年的Java Coding史了,至今还不得不常常感慨于String类带给我的神奇之旅。我认为这是一个非常有趣的类。说String有趣,是因为它构造的对象具有一些其他对象所没有的特征。
我们都知道,创建String对象有两种方式:
String strA = "Hello";
String strA = new String("Hello");
但是,JVM对这两种创建方式有不同的处理。
我们先来做第一个测试,这也是CSDN上日经讨论的主题。奇怪的是这个主题每次都能引起热烈跟贴,无论资深Coder抑或小菜鸟都乐此不疲。
下面,请听题。猜猜下面一段代码输出结果是什么?
public class StringTest1 {
public static void main(String[] args) {
String strA, strB;
strA = "Hello";
strB = "Hello";
System.out.println(strA == strB);
strA =new String("Hello");
strB =new String("Hello");
System.out.println(strA == strB);
strA = strA.intern();
strB = strB.intern();
System.out.println(strA == strB);
}
}
程序输出如下:
true
false
true
程序第11行的输出结果为false,这很好理解,因为strA和strB是两个不同的对象,这是教科书上告诉我们的。那么第7行和第15行为什么输出结果是true呢?
这里需要先介绍一下Java的常量池(ConstantPool)。JDK在编译好的.class文件中,开辟了一个区域,用来存储程序中使用的各种常量。程序运行时,JVM为每个被装载的类维护了一个常量池,该常量池初始时存储了编译期生成并保存在.class文件中的各种常量;其中储存String类型常量的区域,习惯上称之为字符串缓冲池(String Pool)。后面我们会看到,可以通过String类的intern()方法将新的字符串常量添加到字符串缓冲池中。
字符串缓冲池只存储字符串,不存储字符串的引用,认识到这一点非常重要。比如第一个测试中的strA和strB引用,虽然在第5行和第6行时被指向了"Hello",但它们都是在解释程序执行时才赋予地址的。当一个字符串常量被创建时,JVM首先在字符串缓冲池中检查同样的字符串常量是否已存在,若已存在,则不再创建。换句话说,字符串缓冲池中的每个字符串常量值是唯一的。
回到第一个测试的输出结果。当.java文件被编译时,JVM在字符串缓冲池中插入了一个值为"Hello"的字符串。解释程序执行时,解释到第3行,JVM就在内存中声明了两个字符串变量的引用strA和strB;解释到第5行时,JVM在字符串缓冲池中找到了常量字符串"Hello",并把它的引用赋给strA;解释到第6行时,JVM把同样的引用赋给了strB,因此,strA和strB指向了字符串缓冲池中的同一个对象"Hello",故输出结果是true。
而第9和第10行代码中,JDK依然会在编译时找到并处理其中的字符串常量"Hello",在字符串缓冲池中检查该字符串是否存在,若不存在,则创建一个该字符串常量;当程序执行时,JVM会在heap中创建一个String对象,其值为"Hello",并把该对象的引用返回给strA或strB。因此,strA和strB被赋予了两个不同的对象,因而输出结果是false。所以,如果使用new运算符,那么JDK将创建两个String对象,一个在编译期,放入字符串缓冲池中;另一个在解释期,放入heap中。
在第13和第14行代码中,intern()方法的作用是将调用它的字符串对象包含的字符串加入到字符串缓冲池中,再返回这个字符串的引用;如果字符串缓冲池中已包含该字符串,则直接返回该字符串的引用。因此,第13和第14行返回的是字符串缓冲池中同一个对象"Hello"的引用,故strA和strB是相等的。
基于以上的认识,我们来做第二个测试,看看这回输出什么。
StringTest2.java
public class StringTest2 {
public static void main(String[] args) {
String strA, strB, strC, strD;
strA = "Hello World";
strB = "Hello";
strC = strB + " World";
strD = "Hello" + " World";
System.out.println(strA == strC);
System.out.println(strA == strD);
}
}
输出结果是:
false
true
对于程序的第8行,JDK在编译时将其优化为"Hello World",这样strA和strD就指向了字符串缓冲池中的同一个字符串常量。
请注意,strA、strB、strC和strD这几个引用都是在运行时才分配地址的,所以对于strC,JDK无法在编译时作出同样的处理。因此,strC的创建过程是,在解释阶段,通过strB取出字符串缓冲池中"Hello",然后通过“+”运算符,在heap中创建了一个新的对象"Hello World",并把该对象的引用赋给strC。这时候,strA和strC并不指向同一个对象,所以并不相等。
第三个测试是关于String类的toUpperCase()和toLowerCase()方法的。请先看下面的代码并猜猜看输出结果是什么。
StringTest3.java
public class StringTest3 {
public static void main(String[] args) {
String strA, strB;
strA = "Hello";
strB = strA.toUpperCase();
System.out.println(strA == strB);
strA = "HELLO";
strB = strA.toUpperCase();
System.out.println(strA == strB);
strA = "Hello";
strB = strA.toLowerCase();
System.out.println(strA == strB);
strA = "hello";
strB = strA.toLowerCase();
System.out.println(strA == strB);
}
}
输出结果是:
false
true
false
true
感到奇怪吗?String类的toUpperCase()和toLowerCase()方法被设计成:构造原始String的大写/小写形式,作为一个新的String对象返回;但如果新的对象与原始String对象没有差别,则返回原始String对象。所以在上面的代码中,第5~7行的strA和strB是两个不同的对象;而第9~11行的strA和strB是同一个对象。同理可分析第13~19行的代码。
第四个测试是关于equals()方法的,同时涉及到StringBuffer类,因为它是被推荐的用于替代String的理想的类——当需要处理字符串可变情况时。
StringTest4.java
public class StringTest4 {
public static void main(String[] args) {
String strA;
StringBuffer strbufA;
strbufA =new StringBuffer("Hello");
strA = strbufA.toString();
System.out.println(strA.equals(strbufA));
System.out.println(strbufA.equals(strA));
}
}
我们打开String类的源代码,找到equals()方法,代码如下。
String.java - equals()
public boolean equals(Object anObject) {
if (this == anObject) {
returntrue;
}
if (anObjectinstanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}
可见String的equals()方法在比较具体的字符串时,要先判断参数传入的对象是否是一个String对象,因此第四个测试程序第8行输出的是false。同理可以分析第9行的输出。