JAVA(1)【基础类型】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
posted @ 2021-03-29 20:32  小拙  阅读(155)  评论(0编辑  收藏  举报