• 【JAVA】【基础类型】String类型的字符串池


    参考:
    https://www.cnblogs.com/wulouhua/p/3875630.html
    https://blog.csdn.net/xiamiflying/article/details/82860721

    通过如下几个样例,来理解Java中的String定义,在内存中申请多少个对象。

    1、样例1

       String str1="abc";
       String str2="abc";
    

    如上代码,会创建几个String对象呢? 答案是1个

    这个就涉及到了Java中两个关键内容:

    1. 字符串池。
      在JVM中存在着一个 字符串池,其中保存着很多String对象,且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。
      字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
    2. 文本化创建String对象。(暂且这么叫吧)

    解析如上的创建过程,就需了解堆、栈的作用:

      • 存储的是对象,每个对象都包含一个与之对应的class。
      • JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。
      • 对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定
      • 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)。
      • 每个栈中的数据(原始类型和对象引用)都是私有的。
      • 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
      • 数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会自动消失。
    1. 方法区
      • 静态区,跟堆一样,被所有的线程共享。方法区中包含的都是在整个程序中永远唯一的元素,如class、static变量
      • 字符串常量池则存在于方法区。
      • 代码:堆栈方法区存储字符串。

    如上:

    String str1="abc";
    

    字符串池:"abc" ,新建1个String对象
    堆:无
    引用:str1 :新建1个
    (1)首先在 栈 中创建str1变量。
    (2)然后判断 "abc"字符串常量在 字符串池 中是否存在,判断依据是String类equals(Object obj)方法的返回值。
    * 如果存在,不再创建新的对象,直接返回已存在对象的引用,str1直接指向 字符串池 中的 "abc"(此过程是编译器优化的);
    * 如果不存在,在 字符串池 中创建“abc"对象,str1指向 字符串池 中的对象。 (根据上下文,执行前 字符串池 中无此对象,所以此处会创建1个对象)。

    如上:

    String str2 = "abc";
    

    字符串池:"abc" ,已存在,无新建
    堆:无
    引用: str2 :新建1个
    同如上(1)(2)过程,此时 字符串池中已经有"abc"对象,str2直接指向即可。所以此语句不会创建对象。

    2、样例2

    String str=new String("abc");
    

    如上代码,会创建几个String对象呢? 答案是2个

    原因:
    可以把如上这行代码拆分成几部分看待:String str、=、"abc"和new String()。
    (1)String str只是定义了一个名为str的String类型的变量,并没有创建对象;
    (2)=是对变量str赋值,将某个对象的引用赋值给它,也没有创建对象;
    (3)只剩下new String("abc")了。new String("abc")是如何操作的呢?来看一下String的构造器:

         public String(String original) {   
             //other code ...   
         }
    
    所以,此部分是通过new调用了String类的上面那个构造器方法创建了一个对象。
    同时构造器方法的参数也是一个String对象,这个对象内容是"abc"。所以,是是创建了两个对象。
    构造方法的参数的String对象是通过"abc"赋值的,其也是**文本化创建对象**,其也是在 **字符串池** 中创建,然后original变量指向 字符串池 中对象。
    
    So,如上代码是:
     * 字符串池:"abc" : 新建1个String对象
     * 堆:new String:新建1个String对象
     * 引用: str :新建1个
    

    扩展1

       String str1 =  "abc";
       String str2 = new String("abc");
    

    第二句创建几个对象呢?1个。
    因为,第一句已经在 字符串池 中创建了"abc"。第二句只在堆中new String创建一个对象。

    扩展2

       String str2 = new String("ABC") + "ABC" ; 
    

    字符串常量池:"ABC" : 1个String对象
    堆:new String :1个String对象
    引用: str2 :1个

    3. 样例3

       String a="ab"+"cd";  
    

    如上代码,会创建几个String对象呢? 3个吗(“ab”、“cd”、“abcd”)? 答案是1个。

    原因:反编译代码后,我们发现代码是
    String a = "abcd";
    因为 对于静态字符串的连接操作,Java在 编译 时会进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。
    JVM执行到这一句时, 就在 字符串池 中找"abcd",找不到会在 字符串池 中创建1个String对象、值为"abcd"。
    字符串常量池:"abcd":1个String对象。(java做了静态拼接,所以不是在 字符串池 中"ab"和"cd"分别创建一个对象,“+”连接后又创建了一个"abcd"对象)
    堆:无
    引用:a ,1个

    同理:String s = “a” + “b” + “c” + “d” + “e”; 也是在 字符串池 中创建1个String对象。

    扩展:
    是不是所有经过 + 连接后的字符串都会放入 字符串池 呢?
    我们通过如下代码样例来说明。通过 对象引用 进行 == 对比判断是否是引用同一个String对象。

    ==有以下两种情况:
    (1)如果比较的是两个基本类型(char,byte,short,int,long,float,double,boolean),是判断它们的值是否相等。
    (2)如果比较的是两个对象引用,是判断它们的引用是否指向同一个对象。

      public class StringTest {   
          public static void main(String[] args) {   
              String a = "ab"; // 创建了一个对象,并加入字符串池中   
              String b = "cd"; // 创建了一个对象,并加入字符串池中   
              String c = "abcd"; // 创建了一个对象,并加入字符串池中   
       
              String d = "ab" + "cd";   
              if (d == c) {   
                 System.out.println(""ab"+"cd" 创建的对象 "加入了" 字符串池中");   
              } 
              else {  
                 System.out.println(""ab"+"cd" 创建的对象 "没加入" 字符串池中");   
              }   
       
             String e = a + "cd";   
             if (e == c) {   
                System.out.println(" a +"cd" 创建的对象 "加入了" 字符串池中");   
             }   
             else {   
                System.out.println(" a +"cd" 创建的对象 "没加入" 字符串池中");   
             }   
       
             String f = "ab" + b;   
             if (f == c) {   
                 System.out.println(""ab"+ b 创建的对象 "加入了" 字符串池中");   
             }    
             else {   
                 System.out.println(""ab"+ b 创建的对象 "没加入" 字符串池中");   
             }   
       
             String g = a + b;   
             if (g == c) {   
                System.out.println(" a + b 创建的对象 "加入了" 字符串池中");   
             }   
             else {   
                System.out.println(" a + b 创建的对象 "没加入" 字符串池中");   
             }   
         }   
      } 
    

    运行结果如下:
    "ab"+"cd" 创建的对象 "加入了" 字符串池中
    a +"cd" 创建的对象 "没加入" 字符串池中
    "ab"+ b 创建的对象 "没加入" 字符串池中
    a + b 创建的对象 "没加入" 字符串池中
    从上面的结果中看出,只有使用 引号文本方式 使用“+”连接 产生的新对象才会被加入字符串池中。因此提倡大家用 引号文本方式 来创建String对象,以提高效率,实际上这也是我们在编程中常采用的。

    4. 样例4:String类型对象通过StringBuilder拼接

        String a= "a";
        String b= "b";
        String c= "c";
        String d= "d";
        String str = a + b + c + d; 这句创建几个对象呢? 答案是3个对象,但只有1个String对象。
    

    由于编译器的优化,最终代码为通过StringBuilder完成:

          StringBuilder builder = new StringBuilder();    //这里创建了1个StringBuilder对象。
          builder.append(a);
          builder.append(b);
          builder.append(c);
          builder.append(d);
          String str = builder.toString();
    
     我们先看看StringBuilder的构造器
    
        public StringBuilder() {
              super(16);
         }
    

    看下去

         AbstractStringBuilder(int capacity) {
              value = new char[capacity];
         }
    

    可见,构造器分配了一个16字节长度的char数组。

    我们看看append的整个过程:

         public StringBuilder append(String str) {
             super.append(str);
             return this;
         }
    
         public AbstractStringBuilder append(String str) {
          if (str == null)
             str = “null”;
      
          int len = str.length();
          if (len == 0)
              return this;
    
          int newCount = count + len;
          if (newCount > value.length)
            expandCapacity(newCount);
      
          str.getChars(0, len, value, count);
          count = newCount;
    
          return this;
        }
    
        public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
          if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
          }
          if (srcEnd > count) {
            throw new StringIndexOutOfBoundsException(srcEnd);
          }
          if (srcBegin > srcEnd) {
             throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
          }
          System.arraycopy(value, offset + srcBegin, dst, dstBegin, srcEnd - srcBegin);
        }
    

    可见,我们的字符串不会超过16个,所以不会出现扩展value的情况。
    而append里面使用了arraycopy的复制方式,也没有产生新的对象。

    最后,我们再看StringBuilder的 toString()方法:

        public String toString() {
           // Create a copy, don’t share the array
           return new String(value, 0, count);
        }
    

    So,综上所述:三个对象分别为:

        StringBuilder builder = new StringBuilder();  //这里创建了1个StringBuilder对象。  //StringBuilder对象的构造器产生了1个new char[capacity]
        builder.append(a);
        builder.append(b);
        builder.append(c);
        builder.append(d);
        String str = builder.toString();    //这里产生了1个new String。
    

    即,产生了3个对象,其中1个是String对象。

    大家注意:如上默认的16容量,如果题目出现了总长度超过16,则会出现如下的再次分配的情况

       void expandCapacity(int minimumCapacity) {
           int newCapacity = (value.length + 1) * 2;
           if (newCapacity < 0) {
               newCapacity = Integer.MAX_VALUE;
           } else if (minimumCapacity > newCapacity) {
              newCapacity = minimumCapacity;
           }
           value = Arrays.copyOf(value, newCapacity);
       }
    
        public static char[] copyOf(char[] original, int newLength) {
           char[] copy = new char[newLength];
           System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
           return copy;
        }
    

    可见,expand容量时,增加为当前(长度+1)*2。
    注意这里用了Arrays的方法,注意不是前面的System.arraycopy方法。这里产生了一个新的copy的char数组,长度为新的长度。

    5. 样例5:String的intern()方法

      public native String intern();   
    

    这是一个本地方法。在调用此方法时:
    (1)在JDK6中,调用String的intern()方法,如果字符串池中存在该字符串,则直接返回已有字符串的引用;如果没有,会把该字符串对象复制一份放到字符串池中,并返回对象引用。
    (2)在JDK7及以后中,调用String的intern()方法,如果字符串池中存在该字符串,则直接返回已有字符串的引用;如果没有,会把该字符串对象的引用复制一份放到字符串常量池中,并返回字符串常量池中的该引用。

      public class StringExer1 {
          public static void main(String[] args) {
              String s = new String("a") + new String("b");   //new String("ab")
              //在上一行代码执行完以后,字符串常量池中并没有"ab"
     
              String s2 = s.intern();  //jdk6中:在串池中创建一个字符串"ab"
              //jdk8中:串池中没有创建字符串"ab",而是创建一个引用,指向new String("ab"),将此引用返回
     
              System.out.println(s2 == "ab");//jdk6:true  jdk8:true
              // 需要注意的是这里的"ab"并不会再去常量池中重新创建一个字符串变量。因为在常量池中"ab"已经存在(JDK6中复制的对象,是JDK7及以上,创建的堆空间中"ab"对象的引用),为了节约空间,不会重新创建
              System.out.println(s == "ab");//jdk6:false  jdk8:true
          }
      }
    

    通过如上样例可以看出:
    (1)因为对于静态字符串的连接操作,Java在编译时会进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。
    (2)因此要注意StringBuffer/Builder的适用场合:for循环中大量拼接字符串。(如果不用StringBuffer/Builder会频繁创建String对象)
    (3)如果是静态的编译器就能感知到的拼接,采用字符串连接效率更高,不要盲目地去使用StirngBuffer/Builder。

    6. 样例6:字符串和整数拼接什么效果

      String firStr = "123";
      String secStr = firStr + 456;
      System.out.println(secStr);
    

    打印结果是123456

      public class Demo01 {
          public static void main(String[] args) {
              int a = 10;
              int b = 20;
    
              System.out.println(a + b);
    
              a += b;//相当于a = a + b;
              a -= b;//相当于a = a - b;
    
              System.out.println(a);
    
              System.out.println("" + a + b);  //在运算前出现字符串,系统则会将后续出现的变量都转换为字符串进行拼接。 1020
              System.out.println(a + b + "");  //在运算后出现字符串,系统则会先运算,将运算结果转换为字符串再与后面都字符串进行拼接。30
          }
      }
    

    打印结果是:

      30
      10
      1020
      30
    
  • 相关阅读:
    分布式事务
    事务
    shell 脚本编写
    使用fail2ban 防止ssh暴力破解
    数据加密
    英文字符串排序算法
    SpringCloud-ServerConfig 配置中心服务端 / 客户端
    maven setting参考配置
    java面向对象设计原则
    Java Object
  • 原文地址:https://www.cnblogs.com/yickel/p/14594135.html
Copyright © 2020-2023  润新知