zoukankan      html  css  js  c++  java
  • 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比较的情景
  • 相关阅读:
    搭建非域AlwaysOn win2016+SQL2016
    从0开始搭建SQL Server AlwaysOn 第四篇(配置异地机房节点)
    从0开始搭建SQL Server AlwaysOn 第二篇(配置故障转移集群)
    从0开始搭建SQL Server AlwaysOn 第三篇(配置AlwaysOn)
    从0开始搭建SQL Server AlwaysOn 第一篇(配置域控)
    四、基于Windows 2012配置SQL Server 2014 AlwaysOn
    三、安装SQLserver 2014(For AlwaysOn)
    二、 Windows 2012配置故障转移(For SQLServer 2014 AlwaysOn)
    Mybatis-SQL语句构建器类及日志
    Mybatis-JavaAPI
  • 原文地址:https://www.cnblogs.com/cheaptalk/p/12369668.html
Copyright © 2011-2022 走看看