美文网首页
String 类

String 类

作者: gczxbb | 来源:发表于2019-06-01 09:59 被阅读0次

    一、不可变

    String 是不可变类,不可变的意思是 String 类型变量初始化后,其引用指向内存内容不能改变,变量引用可以指向其他内存。

    String str = "abc";
    str = "def";
    
    字符串引用改变地址

    定义一个 String 变量 str,引用指向内存字符串 abc。
    变量赋值时,新开辟内存 def 字符串,str 引用指向新对象,原内存内容 abc 不变。

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        private final char value[];
        ...
    }
    

    String 类是一个字符串数组的封装类(内部一个 char[] 数组)。数组类型 final private,引用 value 值不可变,外部无法访问。

    String 对象内存

    因此,String 对象本质是指向一个 char 数组内存的引用,设计成 final 不可变类型,一旦 String 对象创建,value 值初始化,指向一块字符数组内存,char[] 引用不可变,String 对象即不可改变。

    二、replace() 方法

    替换字符串中的某个字符,String 类 replace() 方法,不直接更改 char[] 引用指向内存,而是开辟一块新内存。

    public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */
    
            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }
    

    创建新 char[] 数组,分配内存,长度和老数组一致,复制,替换,new 一个新 String 类对象,在构造方法,新 char[] 数组赋值 String 类内部 value,返回新 String 引用。

    三、常量池

    class 文件常量池,在文件中,编译器编译生成字面量和符号引用,未载入内存,字面量是文本字符串,(如 String str = "abc" 中的 abc)。
    符号引用是类/接口全限定名,(如 java/lang/String ),变量名称( str ),方法名称和描述符(参数和返回值)。
    类加载内存后,class 文件常量池(字面量和符号引用),进入方法区运行时常量池,该常量池区全局共享。
    字面量(字符串常量池), jdk1.7 后不再方法区,移到堆中,符号引用如方法名、类全限定名仍然在方法区。

    public class StrClass {
        public String a = "hello2";   
        public String a1 = "hello2";   
        public String a2 = "hello"+2;   
    }
    

    定义一个 String 变量 a,编译后 hello2 是文本字符串,在 class 文件常量池,编译阶段确定 a 的值。
    两个字符串字面值,编译时会进行优化(拼接),解析成一个,所以 a2 在编译期由编译器直接解析为 hello2。
    反编译 javap -verbose StrClass.class 命令,查看 class 文件常量池。

    Constant pool

    编译时会检查常量池是否已存在 hello2 字符串,只有一个 #2,String,对应 #22,即 hello2。

    class code

    类加载内存时

    (运行时)在对象初始化阶段,初始化 Code,在常量池中 ldc 获取第 #2 项( hello2 字符串对象),putfield 将引用入栈,分别是 a(#3),a1(#4) 和 a2(#5),它们指向常量池中相同的对象 hello2,(a==a1==a2)。

    此过程会查找字符串常量池是否存在 hello2,若不存在,在堆创建 char[] 数组,创建 String 对象关联 char[] 数组,保存到字符串常量池,最后将a指向这个对象。

    public class StrClass {
        public String a = "hello2";  
     
        public String b = "hello"; 
        public String a3 = b + 2;   
    
        public final String c = "hello"; 
        public String a4 = c + 2; 
    }
    

    编译阶段,不能确定 a3 的值,定义 final 变量 c,字节码替换掉 a4 中的 c 变量,场景和 a2 一致。

    class code

    (运行时)对象变量初始化,new 一个 StringBuilder 对象,a3 引用指向 toString() 方法在堆内存 new 的 String 对象。a==a4,指向字符串常量池,a3 指向堆内存 new 的 String 对象。

    public class StrClass {
        public String a = "hello2";     
        public String a5 = new String("hello2"); 
    }
    

    类加载时,在常量池创建对象 hello2,变量 a5,运行时堆内存 new一个 String 对象,字符串 hello2 已经在常量池,#2项,a 引用指向字符串常量池,a5 引用指向堆内存新对象,(a!=a5)。

    new 关键字创建字符串对象, 在堆内存创建。
    查找常量池是否存在,若没有,创建一个字符串对象,先放入常量池,然后再堆中创建对象,返回堆中的地址,因此,如果常量池中原来没有,会产生两个对象,否则,产生一个对象(堆中)。

    public class StrClass { 
        public String a6 = new String("hello2"); 
    }
    

    class 文件常量池,hello2 文本字符。

    Constant pool

    类加载内存时,在字符串常量池创建一个 hello2 字符串对象。

    初始化Code

    对象初始化时,new 指令,在堆中再次创建一个对象,变量 a6 引用指向它。

    public class StrClass { 
        public String a7 = new String("hello")+new String("2");
    }
    

    class 文件常量池只有 hello 和 2 字符串,没有 hello2 字符串,当类加载时,在字符串常量池不存在 hello2 对象。
    初始化时,new 指令在堆创建两个 String 对象( hello和2 ),通过 StringBuffer 类 append() 方法,toString() 方法在堆内存中 new 一个 String 对象 (hello2),a7 引用指向它。

    四、StringBuffer 和 StringBuilder

    前一节的变量 a3=b+2 赋值时,class 字节码中定义了一个 StringBuilder 类,调用两次 append() 方法,依次添加 b 和 2 ,即 hello 和 2,一次 toString() 方法,堆内存创建对象。
    StringBuffer 和 StringBuilder 区别是线程安全。

    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("hello");
    stringBuffer.append(2);
    

    StringBuffer 通过 char[] 数组保存数据,每一个 append() 方法的新增数据在 char[] 数组保存,支持不同类型,boolean 类型保存4或5个字符 (true/false),字符串将每个字符保存,StringBuffer 类可以对字符串进行修改,进行字符串拼接时,不会产生新对象,直接对 char[] 数组进行操作更改。

    public synchronized StringBuffer append(String str) {
        super.append(str);
        return this;
    }
    

    几乎所有的字符操作方法都 synchronized 同步,该类线程安全。

    @Override
    public synchronized String toString() {
        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);
        }
        return new String(toStringCache, true);
    }
    

    toString() 方法,创建一个 String 对象,关联 char[] 数组。


    任重而道远

    相关文章

      网友评论

          本文标题:String 类

          本文链接:https://www.haomeiwen.com/subject/lwditctx.html