大多数情况下,程序员都会用String类对象表示一个字符串。虚拟机在存储String类对象时会创建一个常量池,把符合条件的对象都存储到常量池中。所谓常量池是指一块用于保存对象的内存区域,这个区域中存储的对象可以被反复利用。这就如同把使用过的工具存放到工具箱,当下次使用工具的时候直接从工具箱中取出就可以,不需要重新创造一个工具。
当程序刚启动时,常量池中没有任何对象,在程序的执行过程中,虚拟机会把程序中出现的字符串常量放入常量池中,而那些不是字符串常量的String类对象则不会被保存到常量池中,例如:
- Scanner sc = new Scanner(System.in);
- String s1 = "abc";//①
- "xyz".charAt(2);//②
- String s2 = sc.nextLine();//③
在这段代码中,出现了两个字符串常量,分别是语句①中的“abc”和语句②中的“xyz”。前文讲过:一个字符串常量就是一个String类对象,因此“abc”和“xyz”都是String类对象,它们以字符串常量的形式出现,所以都会被存放到常量池中。而语句③中的s2虽然也是String类对象,但它是用户从控制台上输入的字符串,不是字符串常量,所以s2不会被存放到常量池中。常量池中的字符串不会出现重复,所以如果常量池中已经有了字符串“abc”,即使程序中再次出现了“abc”,虚拟机也不会在常量池中再次创建一个一模一样的“abc”字符串。
如果一个字符串的值能够在编译阶段就能够确定下来,那么这个字符串也会被加入到常量池中,请看下面的代码:
String s = "abc" + "xyz";
在这段代码中,字符串s是由“abc”和“xyz”这两个字符串常量组成的,因此“abc”和“xyz”这两个字符串都会被加入到常量池中。而由“abc”和“xyz”拼接而成的字符串“abcxyz”也会被加入到常量池中。这是因为字符串常量“abc”和“xyz”的拼接结果只能是“abcxyz”,这个拼接结果在编译阶段就能确定。
如果字符串在拼接过程中出现了引用,只要引用指向的是一个字符串常量,并且在引用前面添加了final关键字,那么这个拼接出来的字符串也会被加入到常量池中,例如:
- final String s1 = "abc";
- final String s2 = "xyz";
- String s = s1 + s2;
在这段代码中,字符串s是通过引用s1和s2拼接而成,s1和s2都指向了字符串常量,并且这两个引用前面都添加了final关键字,这样的话s1和s2拼接而成的字符串“abcxyz”也会被加入到常量池中。s1和s2拼接成的字符串之所以会被加入常量池,是因为String类是不可变类,所以s1和s2所指向的字符串的值不会发生改变,而s1和s2的前面又添加了final关键字,这使得s1和s2的指向也不会发生改变,以上两个条件保证了s1和s2的拼接结果在编译阶段就能够被确定下来,所以这个拼接结果会被加入常量池。
如果是通过String类的构造方法去创建字符串对象,那么每次调用构造方法都会创建出一个新的字符串对象,并且创建出来的字符串对象不属于字符串常量,这个对象自然也不会被加入常量池。例如:
String s = new String(new char[]{'a','b','c'}) ;
在这条语句中,字符串s是通过构造方法创建出来的,所以它不是字符串常量,也不会被加入常量池中。但如果在创建对象时以字符串常量作为构造方法的参数,就会导致有时候用一条语句却能创建出两个字符串对象的现象,例如:
String s = new String("abc");
在这条语句中,使用new关键字创建了字符串对象s,s不是字符串常量,而创建字符串s时使用的参数“abc”却是一个字符串常量,这个字符串常量也是被虚拟机所创建出的字符串对象,并且这个对象与s不是同一个对象。因此,这条语句就会一次性创建两个字符串对象,字符串常量“abc”会被存放在常量池中,字符串s则被存放在常量池之外。
前文讲过:常量池中任意两个字符串的值都不相等,也就是说,如果常量池中已经有了一个值为“abc”的字符串,就不会出现第二个值为“abc”的字符串,因此以下代码会不创建出4个字符串对象:
- String s1 = new String("abc");
- String s2 = new String("abc");
代码中的这两条语句只能创建出3个字符串对象,它们分别是s1、s2以及字符串常量“abc”,由于常量池中不会出现两个值想相同的字符串对象,所以代码中的两个字符串常量“abc”实际上是同一个对象。
如果一个字符串对象不在常量池中,并且这个字符串的值在常量池中没有出现过,那么使用intern()方法可以把这个字符串对象加入到常量池中。例如:
String s = new String(new char[]{'a','b','c'}) ;
这条语句中字符串对象s是通过new关键字创建出来的,所以s不在常量池中,如果常量池中不存在值为“abc”的字符串对象,那么执行了“s.intern();”之后,s就会出现在常量池中。
本小节重点讲解了常量池的概念和原理,下面的【例09_15】能够帮助读者深刻理解哪些字符串会被存入常量池中。
【例09_15 字符串比较】
Exam09_15.java
- public class Exam09_15 {
- public static void main(String[] args) {
- String s1 = "abc";
- String s2 = "abc";
- final String s3 = "abc";
- String s4 = new String("abc");
- String s5 = new String("abc");
- String s6 = "xyz";
- final String s7 = "xyz";
- String s8 = "abc"+"xyz";
- String s9 = "ab"+"cxyz";
- String s10 = new String(new char[] {'h','e','l','l','o'});
- String s11 = new String(new char[] {'h','e','l','l','o'});
- s10.intern();
- s11.intern();
- System.out.println(s1==s2);//①true
- System.out.println(s1==s4);//②false
- System.out.println(s4==s5);//③false
- System.out.println(s8=="abcxyz");//④true
- System.out.println(s8==s9);//⑤true
- System.out.println(s1+s6=="abcxyz");//⑥false
- System.out.println(s3+s7=="abcxyz");//⑦true
- System.out.println(s10=="hello");//⑧true
- System.out.println(s11=="hello");//⑨false
- }
- }
【例09_15】对多个字符串进行了比较操作,通过比较结果就能看出哪些字符串被加入到常量池中。为方便读者阅读程序,本例的代码中直接以注释的形式把比较结果标注到了语句的后面。下面就逐条分析这些比较结果都能证明哪些结论。
语句①用==对两个字符串常量“abc”做比较,比较结果为true。这证明值相同的字符串常量都是同一个对象,并且它们都会被加入常量池中。
前文讲过:字符串常量一定会被加入到常量池中,这就可以推导出:如果一个字符串对象与一个字符串常量用==进行比较的结果为true,就说明这个字符串对象与字符串常量是同一个对象,进而说明这个字符串对象一定在常量池中。根据“常量池中不会出现值相同的字符串”能推导出:如果一个字符串对象的值与一个字符串常量的值相同,但这个字符串对象与字符串常量用==进行比较的结果为false,那么这个字符串对象一定不在常量池中。
语句②用一个字符串常量s1和一个用构造方法创建出的字符串对象s4做比较,虽然s1和s4的值相同,但比较结果为false,这就充分证明用构造方法创建出的字符串对象一定不在常量池中,
语句③用两个用构造方法创建出的字符串做比较,比较结果为false。这证明每次使用构造方法都会创建出一个新的字符串对象,虽然它们的值完全相同,但它们却是两个不同的对象。
语句④用s8和字符串常量“abcxyz”做比较,比较结果为true。s8是一个由字符串常量拼接而成的字符串,它的值在编译阶段就能被确定。比较结果为true就能证明编译阶段就能确定值的拼接字符串也会被加入常量池中。
语句⑤用s8和s9做比较,比较结果为true。虽然这两个字符串都是由字符串常量拼接而成,但s8由“abc”和“xyz”,而s9由“ab”和“cxyz”拼接而成,比较结果为true就能证明即使用不同的字符串常量进行拼接,只要拼接结果相同,并且这个拼接结果在编译阶段就能确定,那么拼接起来的字符串都是同一个对象,这个对象也会被加入到常量池中。
语句⑥用拼接而成的字符串与字符串常量“abcxyz”做比较,比较结果为false。这个比较结果证明:如果在拼接过程中出现了引用,每个引用虽然也都指向了字符串常量,但引用前面没有加final关键字,那么通过引用拼接起来的字符串不会被加入到常量池中。
语句⑦是语句⑥的反例,虽然也是由拼接而成的字符串与字符串常量“abcxyz”做比较,但比较结果为true,这证明如果在拼接过程中出现了引用,每个引用都指向了字符串常量,并且引用前面添加了final关键字,那么通过引用拼接起来的字符串就能在编译阶段把值确定下来,而这个拼接而成的字符串也一定会被加入到常量池中。
语句⑧用一个字符串对象与字符串常量“hello”做比较,比较结果为true。这个比较结果证明如果常量池中没有出现值相同的字符串,那么一个用构造方法创建出的字符串对象在调用了intern()方法后会被加入常量池。
语句⑨与语句⑧本质上是相同的,但比较结果为false。这是因为s10在执行了intern()方法后,常量池中已经有了值为“hello”的字符串,那么s11这个值为“hello”的字符串就不会被重复加入到常量池中。
【例09_15】通过实际的运行结果证明了哪些字符串会被加入常量池中,各位读者需仔细体会字符串被加入常量池的各种条件。通过这个示例也可以看出:常量池中的字符串不会重复,并且可以反复使用,因此最大程度的节约了存储空间。因此各位读者在创建对象时要尽量使用字符串常量赋值的方式创建,而不是使用构造方法创建。