JAVA-字符串的构建

本文所有内容基于以下几个要点

  • String不可变
  • String重载了"+"
  • 对象进行字符串相加会调用toString方法
  • StringBuilder 和 StringBuffer
  • JVM 常量池

阅读完本文,你将对java字符串创建、字符串相加、jvm常量区(内存区域)有一个新的认识,提前感谢你的阅读。

String注意事项

不可变String

  • String类中每一个特看起来会修改String值的方法,实际上都是创建了一个权限的String对象,而最初的String对象丝毫未动

    • 这意味着String是只读的,指向String的任何引用都不会改变它的值
    • 不可变性会带来效率问题(字符串相加的中间对象)
  • String中值的保存的源码,final字符数组意味着value的不可变

    /** The value is used for character storage. */
    private final char value[];
  • 字符串常量池
    • 每个字符序列如"123"会生成一个实例放在常量池里,这个实例是不在堆里的,也不会被GC
    • 我们在使用诸如String str = "abc"的格式定义类时,总是想当然地认为,创建了String类的对象str。但是由于字符串常量池的存在,担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。 只有通过new()方法才能保证每次都创建一个新的对象,因此在使用 "==" 进行对象比较时需要注意这个问题
    • JVM中的常量池在内存当中是以表(hashtable)的形式存在的, 对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值(注意:该表只存储文字字符串值,不存储符号引用。
      常量池中保存着很多String对象,并且可以被共享使用,因此它提高了效率)。
    • String类有一个intern()方法,它可以访问字符串池。是扩充常量池的 一个方法。当一个String实例str调用intern()方法时,Java 查找常量池中 是否有相同的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个等于str的字符串并返回它的引用;

String重载了“+”

  • String的“+”和“+=”是Java中仅有的两个重载过的操作符,java不允许程序员重载任何操作符

  • 当有字符串常量进行相加时,会产生大量的中间对象,所有的这些字符串常量都会保存到 JVM 常量区,此方式产生了大量需要垃圾回收的中间对象

    • “abc”和mango生成一个中间变量x,x和“def”生成一个新的中间变量,以此类推
    String mango = "mango";
    String s = "abc" + mango + "def" + 47;
    System.out.println(s);
    
  • 编译器会对字符串相加自动引入StringBuilder类,因为他更加高效。(详细的分析可以在这篇文章找到 String concatenation with Java 8

    • 但是编译器并不知道需要创建几个StringBuilder,在循环中进行字符串相加会创建大量的StringBuilder对象,这同样需要大量的GC

      String result = "";
      for(int i=0;i<fields.length;i++){ // String[] fields ....
          result += fields[i]; // 每一次循环会创建一个新的StringBuilder
      }
      

      (此处需要依次javap反编译查看JVM字节码)

      但是手动创建StringBuilder却只需要一个对象就可以完成操作

      StringBuilder result = new StringBuilder();
      for(int i=0;i<fields.length;i++){
          result.append(fields[i]); // 仅有一个StringBuilder对象
      }
      result.toString() // get target string
      

      (此处需要依次javap反编译查看JVM字节码)

toString & 无意识递归

  • 无意识递归问题

    • 发生示例如下。this在这里会被进行类型转换,由A类转换成String,但转换正是通过toString来进行的,于是发生递归调用。

      public class A{
          ...
          public String toString(){
              return "toString memory address: "+ this;
          }  
          ...
      }
      
      

      如果期望打印对象地址应当使调用Object.toString()用 super.toString()而不是this.

StringBuilder & StringBuffer

  • StringBuilder允许预先指定大小,如果知道最终字符串大小有多长,那么预先指定StringBuilder的大小会避免多次重新分配缓冲

  • StringBuilder常用的方法:append()、toString()、delete()

  • StringBuffer是线程安全的,但是开销大

  • StringBuilder和StringBuffer均继承自AbstractStringBuilder

    • AbstractStringBuilder类值的保存的源码,可见对象是可变的
      	/**
           * The value is used for character storage.
           */
          char[] value;
      

END

何时使用"+"和StringBuilder

  • 使用 “+” 连接字符串
    • 少量数据
    • 没有循环
  • 使用StringBuilder
    • 单线程
    • 大量数据相加
    • 大量循环
  • 使用StringBuffer
    • 多线程
    • 同StringBuilder

更加深入的了解

  • 需要了解JVM内存模型
  • 需要查看String、StringBuilder、StringBuffer源码
  • 可能需要做一些习题

可供练习的习题

public static void main(String[] args) {  
    /** 
         * 情景一:字符串池 
         * JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象; 
         * 并且可以被共享使用,因此它提高了效率。 
         * 由于String类是final的,它的值一经创建就不可改变。 
         * 字符串池由String类维护,我们可以调用intern()方法来访问字符串池。  
         */  
    String s1 = "abc";     
    //↑ 在字符串池创建了一个对象  
    String s2 = "abc";     
    //↑ 字符串pool已经存在对象“abc”(共享),所以创建0个对象,累计创建一个对象  
    System.out.println("s1 == s2 : "+(s1==s2));    
    //↑ true 指向同一个对象,  
    System.out.println("s1.equals(s2) : " + (s1.equals(s2)));    
    //↑ true  值相等  
    //↑------------------------------------------------------over  
    /** 
         * 情景二:关于new String("") 
         *  
         */  
    String s3 = new String("abc");  
    //↑ 创建了两个对象,一个存放在字符串池中,一个存在与堆区中;  
    //↑ 还有一个对象引用s3存放在栈中  
    String s4 = new String("abc");  
    //↑ 字符串池中已经存在“abc”对象,所以只在堆中创建了一个对象  
    System.out.println("s3 == s4 : "+(s3==s4));  
    //↑false   s3和s4栈区的地址不同,指向堆区的不同地址;  
    System.out.println("s3.equals(s4) : "+(s3.equals(s4)));  
    //↑true  s3和s4的值相同  
    System.out.println("s1 == s3 : "+(s1==s3));  
    //↑false 存放的地区多不同,一个栈区,一个堆区  
    System.out.println("s1.equals(s3) : "+(s1.equals(s3)));  
    //↑true  值相同  
    //↑------------------------------------------------------over  
    /** 
         * 情景三:  
         * 由于常量的值在编译的时候就被确定(优化)了。 
         * 在这里,"ab"和"cd"都是常量,因此变量str3的值在编译时就可以确定。 
         * 这行代码编译后的效果等同于: String str3 = "abcd"; 
         */  
    String str1 = "ab" + "cd";  //1个对象  
    String str11 = "abcd";   
    System.out.println("str1 = str11 : "+ (str1 == str11));  
    //↑------------------------------------------------------over  
    /** 
         * 情景四:  
         * 局部变量str2,str3存储的是存储两个拘留字符串对象(intern字符串对象)的地址。 
         *  
         * 第三行代码原理(str2+str3): 
         * 运行期JVM首先会在堆中创建一个StringBuilder类, 
         * 同时用str2指向的拘留字符串对象完成初始化, 
         * 然后调用append方法完成对str3所指向的拘留字符串的合并, 
         * 接着调用StringBuilder的toString()方法在堆中创建一个String对象, 
         * 最后将刚生成的String对象的堆地址存放在局部变量str3中。 
         *  
         * 而str5存储的是字符串池中"abcd"所对应的拘留字符串对象的地址。 
         * str4与str5地址当然不一样了。 
         *  
         * 内存中实际上有五个字符串对象: 
         *       三个拘留字符串对象、一个String对象和一个StringBuilder对象。 
         */  
    String str2 = "ab";  //1个对象  
    String str3 = "cd";  //1个对象                                         
    String str4 = str2+str3;                                        
    String str5 = "abcd";    
    System.out.println("str4 = str5 : " + (str4==str5)); // false  
    //↑------------------------------------------------------over  
    /** 
         * 情景五: 
         *  JAVA编译器对string + 基本类型/常量 是当成常量表达式直接求值来优化的。 
         *  运行期的两个string相加,会产生新的对象的,存储在堆(heap)中 
         */  
    String str6 = "b";  
    String str7 = "a" + str6;  
    String str67 = "ab";  
    System.out.println("str7 = str67 : "+ (str7 == str67));  
    //↑str6为变量,在运行期才会被解析。  
    final String str8 = "b";  
    String str9 = "a" + str8;  
    String str89 = "ab";  
    System.out.println("str9 = str89 : "+ (str9 == str89));  
    //↑str8为常量变量,编译期会被优化  
    //↑------------------------------------------------------over  
}

参考

  1. Think in java 第四版 第十三章
  2. String concatenation with Java 8:和 thinking in java 有着大体相同阐述的文章,提供了数学分析和数据
  3. 深入理解Java:String:提供了五个String比较的情景
原文地址:https://www.cnblogs.com/cheaptalk/p/12369668.html