当前位置 博文首页 > weixin_33973609的博客:java基础巩固-浅析String源码及其不可变

    weixin_33973609的博客:java基础巩固-浅析String源码及其不可变

    作者:[db:作者] 时间:2021-07-19 13:30

    字符串可以说是广泛应用在日常编程中,jdk从1.0就提供了String类来创建和操作字符串。同时它也是不可改变类(基本类型的包装类都不可改变)的典型代表。

    源码查看(基于1.8)

    public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
        private final char value[]; //这边只是引用并不是真正对象
        ...
    }
    //首先string类创建的对象是不可变的(一个对象在创建完成后不能再改变它状态,说明是不可变的,并发程序最喜欢不可变量了),
    //里面最主要的成员为char类型的数组
    

    几个构造方法

    //空的构造方法 例如 String a = new String(); a为""空字符
    public String() {
        this.value = new char[0];
    }
    
    //带参构造方法 将源的hash和value赋给目标String
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    

    几个常用经典的String类方法

    1.equals
        //如果引用指向的内存值都相等 直接返回true
        public boolean equals(Object anObject) {
            if (this == anObject) {
                return true;
            }
            //instanceof判断是否属于或子类 但Stringfinal修饰不可继承 只考虑是否为String类型
            //上面说过String的成员为char数组,equals内则比较char类数组元素是否一一相等 
            if (anObject instanceof String) {
                String anotherString = (String)anObject;
                int n = value.length;
                //长度不相等返回false
                if (n == anotherString.value.length) {
                    char v1[] = value;
                    char v2[] = anotherString.value;
                    int i = 0;
                    //从后往前单个字符判断,如果有不相等,返回false
                    while (n-- != 0) {
                        if (v1[i] != v2[i])
                            return false;
                        i++;
                    }
                    return true;
                }
            }
            return false;
        }
    

    2.subString(beginIndex,endIndex)

    subString这边存在一个小插曲

    在jdk1.7以前,该方法存在内存泄漏问题。之所以存在是因为在此之前,String三个参数的构造方法是这么写的。成员变量为这三个,jdk7以后取消掉了offset和count加入了hash,虽然原来的构造方法简洁高效但存在gc问题。所以7以后放弃了性能采取了更为保守的写法。

        /** The value is used for character storage. */
        private final char value[];
    
        /** The offset is the first index of the storage that is used. */
        private final int offset;
    
        /** The count is the number of characters in the String. */
        private final int count;
       ...
       public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > count) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        if (beginIndex > endIndex) {
            throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
        }
     //虽然这边返回的是新的String对象,但构造方法中还引用着原先的value
        return ((beginIndex == 0) && (endIndex == count)) ? this :
            new String(offset + beginIndex, endIndex - beginIndex, value);
        }
       ...
       String(int offset, int count, char value[]) {  
         this.value = value;  
         this.offset = offset;  
         this.count = count;  
        }  
    //这边开始this.value = value; 出现问题,这三个个原来为String类中的三个私有成员变量,因为这种实现还在引用原先的字符串变量value[] 通过offset(起始位置)和count(字符所占个数)返回一个新的字符串,这样可能导致jvm认为最初被截取的字符串还被引用就不对其gc,如果这个原始字符串很大,就会占用着内存,出现内存泄漏等gc问题。
    

    jdk1.7以后的写法变化

    //虽然这边还是有offse和count参数 但不是成员变量了
    private final char value[];
    
    /** Cache the hash code for the string */
    private int hash; // Default to 0
    ...
    public String substring(int beginIndex, int endIndex) {
          if (beginIndex < 0) {
              throw new StringIndexOutOfBoundsException(beginIndex);
          }
          if (endIndex > value.length) {
              throw new StringIndexOutOfBoundsException(endIndex);
          }
          int subLen = endIndex - beginIndex; 
          if (subLen < 0) {
              throw new StringIndexOutOfBoundsException(subLen);
          }
          return ((beginIndex == 0) && (endIndex == value.length)) ? this
                    : new String(value, beginIndex, subLen);//构造函数参数顺序也有所变化
    }
    ...
    public String(char value[], int offset, int count) {
      if (offset < 0) {
        throw new StringIndexOutOfBoundsException(offset);
      }
      if (count < 0) {
        throw new StringIndexOutOfBoundsException(count);
      }
      // Note: offset or count might be near -1>>>1.
      if (offset > value.length - count) {
        throw new StringIndexOutOfBoundsException(offset + count);
      }
        //新的不是引用之前的而是重新新建了一个。
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }
    
    
    3.hashcode
    public int hashCode() {
        int h = hash;
        //如果hash没有被计算过,并且字符串不为空,则进行hashCode计算
        if (h == 0 && value.length > 0) {
            char val[] = value;
            //计算过程
            //s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            //hash赋值
            hash = h;
        }
        return h;
    }
    //String重写了Object类的hashcode方法,根据值来计算hashcode值,不过Object的该方法为native本地方法。
    //该方法设计十分巧妙,它先从0判断字符大小,如果
    //hashcode其实就是散列码 这种算法的话 同样的字符串的值一定相等 但不同的字符串其实也有可能得到同样的hashcode值 
    n=3
    i=0 -> h = 31 * 0 + val[0]  
    i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]
    i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2]
    //以字符串 "123"为例 1的的ascil码为49 所以 "1".hashcode()的值为49,2为50...
    h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2] = 31 * (31 * 49 + 50) + 51 = 48690
    

    详细算法参考这里

    String的不可变性

    //这边可能存在一个疑问 s对象是否发生了改变
    String s = "hello";
    s = "world";
    //String不可变不是在原内存地址上修改数据,而是重新指向一个新对象,新地址,所以这里的hello对象并没有被改变。
    //同样的类似replace方法源码中
      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);//这边返回的是新的一个String对象 而不改变原来的对象
                }
            }
            return this; //如果都一样 就指向同一个对象了
        }
    
    cs