在理解字符串常量前,我们先熟悉一下如何创建一个字符串,在Java中有两种方法可以创建一个字符串对象:
- 使用new运算符。例如:
1 | String str = new String( "Hello" ); |
- 使用字符串常量或者常量表达式。例如:
12 | String str= "Hello" ; //(字符串常量) 或者 String str= "Hel" + "lo" ; //(字符串常量表达式). |
这些字符串的创建方式之间有什么区别呢?在Java中,equals方法被认为是对象的值进行深层次的比较,而操作符==是进行的浅层次的比较。equals方法比较两个对象的内容而不是引用。==两侧是引用类型(例如对象)时,如果引用是相同的-即指向同一个对象-则执行结果为真。如果是值类型(例如原生类型),如果值相同,则执行结果为真。equals方法在两个对象具有相同内容时返回真-但是,java.lang.Object类中的equals方法返回真-如果类没有覆盖默认的equals方法,如果两个引用指向同一个对象。
让我们通过下面的例子来看看这两种字符串的创建方式之间有什么区别吧。
1234567891011121314151617181920212223242526 | public class DemoStringCreation { public static void main(String args[]) { String str1 = "Hello" ; String str2 = "Hello" ; System.out.println( "str1 and str2 are created by using string literal." ); System.out.println( " str1 == str2 is " + (str1 == str2)); System.out.println( " str1.equals(str2) is " + str1.equals(str2)); String str3 = new String( "Hello" ); String str4 = new String( "Hello" ); System.out.println( "str3 and str4 are created by using new operator." ); System.out.println( " str3 == str4 is " + (str3 == str4)); System.out.println( " str3.equals(str4) is " + str3.equals(str4)); String str5 = "Hel" + "lo" ; String str6 = "He" + "llo" ; System.out.println( "str5 and str6 are created by using string constant expression." ); System.out.println( " str5 == str6 is " + (str5 == str6)); System.out.println( " str5.equals(str6) is " + str5.equals(str6)); String s = "lo" ; String str7 = "Hel" + s; String str8 = "He" + "llo" ; System.out.println( "str7 is computed at runtime." ); System.out.println( "str8 is created by using string constant expression." ); System.out.println( " str7 == str8 is " + (str7 == str8)); System.out.println( " str7.equals(str8) is " + str7.equals(str8)); } } |
输出结果为:
12345678910111213 | str1 and str2 are created by using string literal. str1 == str2 is true str1.equals(str2) is true str3 and str4 are created by using new operator. str3 == str4 is false str3.equals(str4) is true str5 and str6 are created by using string constant expression. str5 == str6 is true str5.equals(str6) is true str7 is computed at runtime. str8 is created by using string constant expression. str7 == str8 is false str7.equals(str8) is true |
使用相同的字符序列而不是使用new关键字创建的两个字符串会创建指向Java字符串常量池中的同一个字符串的指针。字符串常量池是Java节约资源的一种方式。
字符串常量池
字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价。JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,每当代码创建字符串常量时,JVM会首先检查字符串常量池。如果字符串已经存在池中,就返回池中的实例引用。如果字符串不在池中,就会实例化一个字符串并放到池中。Java能够进行这样的优化是因为字符串是不可变的,可以不用担心数据冲突进行共享。例如:
123456789 | public class Program { public static void main(String[] args) { String str1 = "Hello" ; String str2 = "Hello" ; System.out.print(str1 == str2); } } |
其结果是:
1 | true |
不幸的是,当使用:
1 | String a= new String( "Hello" ); |
一个字符串对象在字符串常量池外创建,即使池里存在相同的字符串。考虑到这些,要避免new一个字符串除非你明确的知道需要这么做!例如:
12345678910 | public class Program { public static void main(String[] args) { String str1 = "Hello" ; String str2 = new String( "Hello" ); System.out.print(str1 == str2 + " " ); System.out.print(str1.equals(str2)); } } |
结果是:
1 | false true |
JVM中有一个常量池,任何字符串至多维护一个对象。字符串常量总是指向字符串池中的一个对象。通过new操作符创建的字符串对象不指向字符串池中的任何对象,但是可以通过使用字符串的intern()方法来指向其中的某一个。java.lang.String.intern()返回一个保留池字符串,就是一个在全局字符串池中有了一个入口。如果以前没有在全局字符串池中,那么它就会被添加到里面。例如:
123456789101112131415 | public class Program { public static void main(String[] args) { // Create three strings in three different ways. String s1 = "Hello" ; String s2 = new StringBuffer( "He" ).append( "llo" ).toString(); String s3 = s2.intern(); // Determine which strings are equivalent using the == // operator System.out.println( "s1 == s2? " + (s1 == s2)); System.out.println( "s1 == s3? " + (s1 == s3)); } } |
输出是:
12 | s1 == s2? false s1 == s3? true |
为了优化空间,运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用。这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收。
Java语言规范第三版中的字符串常量
每一个字符串常量都是指向一个字符串类实例的引用。字符串对象有一个固定值。字符串常量,或者一般的说,常量表达式中的字符串都被使用方法 String.intern进行保留来共享唯一的实例。
12345678910111213 | package testPackage; class Test { public static void main(String[] args) { String hello = "Hello" , lo = "lo" ; System.out.print((hello == "Hello" ) + " " ); System.out.print((Other.hello == hello) + " " ); System.out.print((other.Other.hello == hello) + " " ); System.out.print((hello == ( "Hel" + "lo" )) + " " ); System.out.print((hello == ( "Hel" +lo)) + " " ); System.out.println(hello == ( "Hel" +lo).intern()); } } class Other { static String hello = "Hello" ; } |
编译单元:
12 | package other; public class Other { static String hello = "Hello" ; } |
产生输出:
1 | true true true true false true |
这个例子说明了六点:
- 同一个包下同一个类中的字符串常量的引用指向同一个字符串对象;
- 同一个包下不同的类中的字符串常量的引用指向同一个字符串对象;
- 不同的包下不同的类中的字符串常量的引用仍然指向同一个字符串对象;
- 由常量表达式计算出的字符串在编译时进行计算,然后被当作常量;
- 在运行时通过连接计算出的字符串是新创建的,因此是不同的;
- 通过计算生成的字符串显示调用intern方法后产生的结果与原来存在的同样内容的字符串常量是一样的。