• 第九章《字符串》第3节:String类对象的存储方式


    大多数情况下,程序员都会用String类对象表示一个字符串。虚拟机在存储String类对象时会创建一个常量池,把符合条件的对象都存储到常量池中。所谓常量池是指一块用于保存对象的内存区域,这个区域中存储的对象可以被反复利用。这就如同把使用过的工具存放到工具箱,当下次使用工具的时候直接从工具箱中取出就可以,不需要重新创造一个工具。

    当程序刚启动时,常量池中没有任何对象,在程序的执行过程中,虚拟机会把程序中出现的字符串常量放入常量池中,而那些不是字符串常量的String类对象则不会被保存到常量池中,例如:

    1. Scanner sc = new Scanner(System.in);
    2. String s1 = "abc";//①
    3. "xyz".charAt(2);//②
    4. 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关键字,那么这个拼接出来的字符串也会被加入到常量池中,例如:

    1. final String s1 = "abc";
    2. final String s2 = "xyz";
    3. 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个字符串对象:

    1. String s1 = new String("abc");
    2. 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

    1. public class Exam09_15 {
    2.     public static void main(String[] args) {
    3.         String s1 = "abc";
    4.         String s2 = "abc";
    5.         final String s3 = "abc";
    6.         String s4 = new String("abc");
    7.         String s5 = new String("abc");
    8.         String s6 = "xyz";
    9.         final String s7 = "xyz";
    10.         String s8 = "abc"+"xyz";
    11.         String s9 = "ab"+"cxyz";
    12.         String s10 = new String(new char[] {'h','e','l','l','o'});
    13.         String s11 = new String(new char[] {'h','e','l','l','o'});
    14.         s10.intern();
    15.         s11.intern();
    16.         System.out.println(s1==s2);//①true
    17.         System.out.println(s1==s4);//②false
    18.         System.out.println(s4==s5);//③false
    19.         System.out.println(s8=="abcxyz");//④true
    20.         System.out.println(s8==s9);//⑤true
    21.         System.out.println(s1+s6=="abcxyz");//⑥false
    22.         System.out.println(s3+s7=="abcxyz");//⑦true
    23.         System.out.println(s10=="hello");//⑧true
    24.         System.out.println(s11=="hello");//⑨false
    25.     }
    26. }

    【例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】通过实际的运行结果证明了哪些字符串会被加入常量池中,各位读者需仔细体会字符串被加入常量池的各种条件。通过这个示例也可以看出:常量池中的字符串不会重复,并且可以反复使用,因此最大程度的节约了存储空间。因此各位读者在创建对象时要尽量使用字符串常量赋值的方式创建,而不是使用构造方法创建。

    除阅读文章外,各位小伙伴还可以点击这里观看我在本站的视频课程学习Java!

  • 相关阅读:
    NR CSI(四) PMI
    汽车行业数字化转型:时代巨变下的新机遇
    [ LeetCode ] 题刷刷(Python)-第58题:最后一个单词的长度
    【Apollo】自动驾驶技术的介绍
    如何做接口测试呢?接口测试有哪些工具
    Kakao账号如何注册使用?如何Kakao多开?外贸必备全面教程
    荣耀进击背后的「韧性力量」
    Pymoo:优化算法收敛性的实例分析
    TikTok搬运视频怎么做,搬运怎样的视频最好
    Chrome开发工具与js加密
  • 原文地址:https://blog.csdn.net/shalimu/article/details/128064831