1、关于字符串的陷阱
字符串是Java程序中使用最广泛的一种对象,虽然它具有简单易用的特征,但是实际使用字符串时也会有一些潜在的陷阱,这些陷阱往往会给实际开发带来潜在的困扰。
1、1 JVM对字符串的处理
String str = new String("Java对象");
对于上面常见的Java语句,常见的问题是,上面语句创建了几个字符串?答案是:上面的语句实际上创建了
两个字符串对象,其中一个是“Java对象”这个直接量对应的字符串对象,另一个是有new String()构造器返回的字符串对象。
提示:Java程序中创建对象的常见方式有如下四种。
- 通过new调用构造器创建Java对象
- 通过Class对象的newInstance()方法调用构造器创建Java对象
- 通过Java的反序列化机制从IO流中恢复Java对象
- 通过Java对象提供的clone()方法复制一个Java对象
public class StringTest {
public static void main(String[] args) {
String str1 = "Java对象";
String str2 = "Java对象";
System.out.println(str1 = str2);
}
}
输出结果:
true
从输出的结果可以看出,str1和str2都指向JVM字符串池中的"Java对象",同时也可以知道两个变量都是指向同一块内存空间。
在之前的学习中可以知道,除了通过字符串直接量创建字符串对象之外,还可以通过字符串连接表达式来创建字符串对象。如果这个字符串连接表达式的值可以在编译时确定下来,那么JVM会在编译时计算该字符串变量的值,并让它指向字符串池中对应的字符串。
public class StringJoinTest2 {
public static void main(String[] args) {
String str1 = "Java对象的长度:6";
String str2 = "Java" + "对象" + "的长度:" + 6;
System.out.println(str1 == str2);
}
}
输出结果:
true
上面程序中的定义的str2是一个字符串连接表达式,但是由于字符串连接表达式可以在编译时就确定下来,因此JVM将在编译时计算str2的值,并让str2指向字符串池中对应的字符串。因此,上面程序最终输出的是true。
注意:在创建str2字符串变量时,它们都是字符串直接量、整数直接量,没有变量参与,没有方法调用。因此,JVM可以在编译时就确定该字符串连接表达式的值,可以让该字符串变量指向字符串池中对应的字符串。但是如果程序使用了变量或者调用了方法,那就只能等到运行时才能确定字符串连接表达式的值,也就无法再编译时就确定该字符串比那辆的值,因此无法利用JVM的字符串池。
public class StringJoinTest3 {
public static void main(String[] args) {
String str1 = "Java对象的长度:6";
String str2 = "Java" + "对象" + "的长度:" + "Java对象".length();
System.out.println(str1 == str2);
int len = 6;
String str3 = "Java" + "对象" + "的长度:" + len;
System.out.println(str1 == str3);
}
}
输出结果为:
false
false
false
上面程序中str2变量对应的连接表达式中包含了一个方法的调用,因此程序无法再编译时确定str2变量的值,也就是不会让str2指向JVM字符串池中对应的字符串。类似地,str3的值也是字符串连接表达式,但由于这个字符串连接表达式中包含了一个len变量,因此str3变量也不会指向JVM字符串池中对应的字符串。
有一种情况例外,如果字符串连接运算中的所有变量都可以执行“宏替换”,那么JVM一样可以在编译时确定字符串连接表达式的值,一样会让字符串变量指向JVM字符串池中的对应字符串。
public class StringJoinTest4 {
public static void main(String[] args) {
String str1 = "Java对象的长度:6";
final String s1 = "Java";
String str2 = s1 + "对象" + "的长度:6";
System.out.println(str1 == str2);
final int len = 6;
String str3 = "Java" + "对象" +"的长度:" + len;
System.out.println(str1 == str3);
}
}
输出结果为:
true
true
总结:当程序中需要使用该字符串、基本类型包装类实例时,应该尽量使用字符串直接量、基本类型值的直接量,避免通过new String()、new Integer()的形式来创建,这样能保证有更好的性能。
1、2 不可变的字符串
String类是一个典型的不可变类。当一个String对象创建完成后,该String类包含的字符序列就被固定下来,以后永远不能改变。
public class ImmutableString {
public static void main(String[] args) {
String str = "Hello"; //①
System.out.println(System.identityHashCode(str));
str = str + "Java对象"; //②
System.out.println(System.identityHashCode(str));
}
}
输出结果为:
134683287
174240671
174240671
从上面两幅图中可以看出,str变量原来指向的字符串对象并没有发生任何改变,所包含的字符串序列依然是"Hello",只是str变量不再指向它而已。换句话来说,发生改变的不是String对象,而是str变量本身,它改变了指向,指向了一个新的String对象。
注意:System提供identityHashCode()静态方法用于获取某个对象的唯一的hashCode值,这个identityHashCode()方法返回值与该类是否重写了hashCode()方法无关。只有当两个对象相同时,它们的identityHashCode值才会相等。
对于String类而言,它代表字符串序列不可改变的字符串,因此如果程序需要一个字符序列会发生改变的字符串,应该考虑使用StringBuilder和StringBuffer。实际上通常应该优先考虑使用StringBuilder。StringBuilder和StringBuffer区别在于,StringBuilder是线程不安全的,但StringBuffer是线程安全的,也就是说,其中大部分方法都增加了synchronized修饰符。对方法增加了synchronized修饰符可以保证该线程安全,但是会降低方法执行效率。
public class MutableString {
public static void main(String[] args) {
StringBuilder str = new StringBuilder("Hello");
System.out.println(str);
System.out.println(System.identityHashCode(str));
str.append("Java对象");
System.out.println(str);
System.out.println(System.identityHashCode(str));
}
}
输出结果为:
Hello
134683287
HelloJava对象
134683287
134683287
HelloJava对象
134683287
1、3 字符串比较
如果程序需要比较两个字符串是否相同,用==进行判断就可以了;但如果要判断两个字符串所包含的字符序列是否相同,则应该使用String重写过的equals()方法进行比较。
//String源代码
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
从String类源代码来看,字符串底层实际上采用一个字符数组来保存该字符串所包含的字符序列,如果当前面字符串和被比较字符串底层的字符数组所包含的字符序列完全相同,程序通过equals()方法判断两个字符串是否相等就会返回true。
除此之外,由于String类还实现了Comparable接口,因此程序还可通过String提供的compareTo()方法来判断两个字符串之间的大小。当两个字符串所包含的字符序列相同时,程序通过compareTo()方法比较,将返回0。
public class StringCompare {
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = new String("def");
String s3 = new String("abc");
if ( s1.compareTo(s3) == 0 ) {
System.out.println("s1和s3包含的字符序列相同");
}
if( s1.compareTo(s2) < 0 ) {
System.out.println("s1小于s2");
}
System.out.println("s1和s3包含的字符序列相同:" + s1.equals(s3));
System.out.println("s1和s3包含的字符序列相同:" + (s1 == s3));
}
}
输出结果为:
s1和s3包含的字符序列相同
s1小于s2
s1和s3包含的字符序列相同:true
s1和s3包含的字符序列相同:false
s1小于s2
s1和s3包含的字符序列相同:true
s1和s3包含的字符序列相同:false