zoukankan      html  css  js  c++  java
  • 【源码】String类、jdk1.6subString内存泄漏、字符串拼接几种区别、

    一、String类源码

    出于安全性考虑,字符串经常作为网络连接、数据库连接等参数,不可变就可以保证连接的安全性
     
    String类实现了3个接口:
      1.实现了io流的Serializable接口,用于表明String类的对象可被序列化.String在实现了Serializable接口之后,所以支持序列化和反序列化支持。Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException).
      2.实现Comparable接口,用于表明String类的对象进行整体排序,定义的泛型为String类,说明给Comparable的数据类型只能是String类
      3.实现CharSequence接口,用于表明char值得一个只读的字符序列。此接口对许多不同种类的char序列提供统一的自读访问。

    string的本质是char[ ]字符数组,String类只是封装字符串的一些操作的,真是的字符串就是存在其下value这个字符数组中的。  

    public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];    //字符数组
       //private final char[] value;
    
       //hash是String实例化的hashcode的一个缓存。字符串的不变性确保了hashcode的值一直是一样的,在需要hashcode时,就不需要每次都计算,这样会很高效。
        private int hash;   //Default to 0
    
        private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
    
        public String() {
            this.value = "".value;  //或者可以写this.value = new char[0];构造里只能初始化长度,不能赋值如: ={'A'};
        }
    
        public String(char value[]) {
            this.value = Arrays.copyOf(value, value.length);
        }
        
      //本质是持有一个静态内部类,用于忽略大小写得比较两个字符串。
        public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
    
        private static class CaseInsensitiveComparator implements Comparator<String>, java.io.Serializable {

        public int compare(String s1, String s2) { int n1 = s1.length(); int n2 = s2.length(); int min = Math.min(n1, n2); for (int i = 0; i < min; i++) { char c1 = s1.charAt(i); char c2 = s2.charAt(i); if (c1 != c2) { c1 = Character.toUpperCase(c1);  //比较时忽略大小写,同一索引的下标先都转为大写比较,再转为小写比较一次 c2 = Character.toUpperCase(c2); if (c1 != c2) { c1 = Character.toLowerCase(c1); c2 = Character.toLowerCase(c2); if (c1 != c2) { // No overflow because of numeric promotion return c1 - c2; } } } } return n1 - n2; } private Object readResolve() { return CASE_INSENSITIVE_ORDER; } } }

      CASE_INSENSITIVE_ORDER 如果需要忽略大写,比较时传入此比较器即可,因为实现了Comparator比较器接口。【Comparator是比较器,实现Comparable的对象自身可以直接使用比较

        或者直接使用来持有这个内部类的公共的静态变量 CASE_INSENSITIVE_ORDER,可以简单得用它来比较两个String,这样当要比较两个String时可以通过这个变量来调用。

      并且String类中提供的compareToIgnoreCase方法其实就是调用这个内部类里面的方法实现的。

      public int compareToIgnoreCase(String str) {
            return CASE_INSENSITIVE_ORDER.compare(this, str);
        }

      通过一个String 内部一个static的内部类实现的,那么为什么还要特地写一个内部类呢,这样其实就是为了代码复用,这样在其他情况下也可以使用这个static内部类。

      因为String本质就是通过char[]实现的,可以发现length(),isEmpty(),charAt()这些方法其实就是在内部调用数组的方法。

      public int length() {
            return value.length;
        }
    
        public boolean isEmpty() {
            return value.length == 0;
        }
    
        public char charAt(int index) {
            if ((index < 0) || (index >= value.length)) {
                throw new StringIndexOutOfBoundsException(index);
            }
            return value[index];
        }

      

      将String转化成二进制:本质上是调用了StringCoding.encode()这个静态方法。

        public byte[] getBytes() {
            return StringCoding.encode(value, 0, value.length);
        }

     

      equals:

      首先进行当前对象和要判断的对象引用的是不是同一个对象(==判断引用地址),如果是则返回true。接着判断要判断的对象是不是String类的实例,如果是,接着转化类型判断两个字符串的长度,如果一样,进行char数组[]的逐一比较。

    public boolean equals(Object anObject) {
            if (this == anObject) {  //首先进行当前对象和要判断的对象引用的是不是同一个对象
                return true;
            }
            if (anObject instanceof String) {
                String anotherString = (String)anObject;  //判断要判断的对象是不是String类的实例
                int n = value.length;
                if (n == anotherString.value.length) {  //如果两个字符串长度一样,那么一一比较char[]
                    char v1[] = value;
                    char v2[] = anotherString.value;
                    int i = 0;
                    while (n-- != 0) {  //先取值,后减减
                        if (v1[i] != v2[i])
                            return false;
                        i++;
                    }
                    return true;
                }
            }
            return false;
        }

      

      hashCode:    

      就是在存储数据计算hash地址的时候,我们希望尽量减少有同样的hash地址。如果使用相同hash地址的数据过多,那么这些数据所组成的hash链就更长,从而降低了查询效率。使用31的原因可能是为了更好的分配hash地址,并且31只占用5bits。在Java中,整型数是32位的,也就是说最多有2^32= 4294967296个整数,将任意一个字符串,经过hashCode计算之后,得到的整数应该在这4294967296数之中。那么,最多有 4294967297个不同的字符串作hashCode之后,肯定有两个结果是一样的。

      hashCode方法可以保证相同的字符串具有相同的hash值。但是hash值相同并不一定是字符串的value值相同。

        public int hashCode() {
            int h = hash;  //初始值为0
            if (h == 0 && value.length > 0) {
                char val[] = value;  //字符数组
    
                for (int i = 0; i < value.length; i++) {
                    h = 31 * h + val[i];
                }
                hash = h;
            }
            return h;
        }

      jdk1.7 Switch选择表达式对String支持原理:

      其实,jdk1.7并没有新的指令来处理switch string,而是通过调用switch中string.hashCode(),将string转换为int类型的hash值。然后用这个Hash值来唯一标识着这个case

      当匹配的时候,首先调用这个字符串的hashCode()方法,获取一个Hash值(int类型),用这个Hash值来匹配所有的case,如果没有匹配成功,说明不存在;如果匹配成功了,接着会调用字符串的equals()方法进行匹配。因为hashCode相同,字符串的value不一定相同

      contentEqauls:

      主要是用来比较String和StringBuffer或者StringBuild的内容是否一样。可以看到传入参数是CharSequence ,这也说明了StringBuffer和StringBuild同样是实现了CharSequence。源码中先判断参数是从哪一个类实例化来的,再根据不同的情况采用不同的方案,不过其实大体都是采用上面那个for循环的方式来进行判断两字符串是否内容相同。

    public boolean contentEquals(CharSequence cs) {
            // Argument is a StringBuffer, StringBuilder
            if (cs instanceof AbstractStringBuilder) {
                if (cs instanceof StringBuffer) {                 synchronized(cs) {  //如果是线程安全的StringBuffer,那么会使用同步代码锁
                       return nonSyncContentEquals((AbstractStringBuilder)cs);
                    }
                } else {  //如果是StringBuilder
                    return nonSyncContentEquals((AbstractStringBuilder)cs);
                }
            }
            // Argument is a String  //如果被比较的只是Sting类型,那么直接调用equals方法
            if (cs instanceof String) {
                return equals(cs);
            }
            // Argument is a generic CharSequence  //如果是一个字符序列CharSequence
            char v1[] = value;
            int n = v1.length;
            if (n != cs.length()) {  //先比较长度
                return false;
            }
            for (int i = 0; i < n; i++) {
                if (v1[i] != cs.charAt(i)) {  //在比较每一个字符
                    return false;
                }
            }
            return true;
        }
    
      //字符串与字符串缓冲区比较
    private boolean nonSyncContentEquals(AbstractStringBuilder sb) { char v1[] = value; char v2[] = sb.getValue(); int n = v1.length; if (n != sb.length()) { return false; } for (int i = 0; i < n; i++) { if (v1[i] != v2[i]) { return false; } } return true; }

      

      compareTo:

      这个就是String对Comparable接口中方法的实现了。

      先通过比较两个字符串的长度将最小的长度赋值给lim,接着将字符串赋值给两个字符数组,在0~lim范围内进行字符数组的逐一判断,如果有一个不相等则返回两个字符的ASCII码的差值,如果循环结束都相等则返回两个长度的差值。其核心就是那个while循环,通过从第一个开始比较每一个字符,当遇到第一个较小的字符时,判定该字符串小。

      注意:anotherString.value 不报错是因为在本类中本类对象的引用可以使用private变量

    public int compareTo(String anotherString) {
            int len1 = value.length;
            int len2 = anotherString.value.length;
            int lim = Math.min(len1, len2);  //
            char v1[] = value;
            char v2[] = anotherString.value;
    
            int k = 0;
            while (k < lim) {
                char c1 = v1[k];
                char c2 = v2[k];
                if (c1 != c2) {
                    return c1 - c2;
                }
                k++;
            }
            return len1 - len2;
        }

      

      startswith:判断当前字符串是否以某一段其他字符串开始的,和其他字符串比较方法一样,其实就是通过一个while来循环比较。

    public boolean startsWith(String prefix, int toffset) {
            char ta[] = value;
            int to = toffset;
            char pa[] = prefix.value;
            int po = 0;
            int pc = prefix.value.length;
            // Note: toffset might be near -1>>>1.
            if ((toffset < 0) || (toffset > value.length - pc)) {
                return false;
            }
            while (--pc >= 0) {
                if (ta[to++] != pa[po++]) {
                    return false;
                }
            }
            return true;
        }

      contact:

      concat的作用是将str拼接到当前字符串后面,通过代码也可以看出其实就是建一个新的字符串。

    public String concat(String str) {
            int otherLen = str.length();
            if (otherLen == 0) {
                return this;
            }
            int len = value.length;
            char buf[] = Arrays.copyOf(value, len + otherLen);
            str.getChars(buf, len);
            return new String(buf, true);
        }

       replace和replaceAll:

      都是全部替换匹配的字符,而replaceAll是通过正则表达式的方式,替换所有匹配的字符

      如果只替换第一个匹配的字符,使用replaceFirst

    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;
        }
    
        public String replaceAll(String regex, String replacement) {
            return Pattern.compile(regex).matcher(this).replaceAll(replacement);
        }

      

      trim:

      字符的比较其实是比较ASCII码值,而空字符对应的值是32,是最小的,比32小那么就是空字符

    public String trim() {
            int len = value.length;
            int st = 0;
            char[] val = value;    /* avoid getfield opcode */
    
            while ((st < len) && (val[st] <= ' ')) {  //去除前缀空字符
                st++;
            }
            while ((st < len) && (val[len - 1] <= ' ')) {  //取出后缀空字符
                len--;
            }
            return ((st > 0) || (len < value.length)) ? substring(st, len) : this;  //根据新的没有空字符的索引来截取原字符串
        }

      string.valueOf(int i)与Integer.toString(int i)本质上没什么区别:

    String:
        public static String valueOf(int i) {
            return Integer.toString(i);
        }
    
    Integer:
        public static String toString(int i) {
            if (i == Integer.MIN_VALUE)
                return "-2147483648";
            int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
            char[] buf = new char[size];
            getChars(i, size, buf);
            return new String(buf, true);
        }

    二、jdk1.6的subString内存泄漏问题:

      在 JDK 1.6 中,java.lang.String 主要由3 部分组成:代表字符数组的value、偏移量offset和长度count

    char[] value
    offset 偏移 
    count 长度

      字符串的实际内容由value、offset 和count 三者共同决定,而非value 一项。如果字符串value 数组包含100 个字符,而count 长度只有1 个字节,那么这个String 实际上只有1 个字符,却占据了至少100 个字节,那剩余的99 个就属于泄漏的部分,它们不会被使用,不会被释放,却长期占用内存,直到字符串本身被回收。可以看到,str 的count 为1,而它的实际取值为字符串“0”,但是在value 的部分,却包含了上万个字节,在这个极端情况中,原本只应该占用1 个字节的String,却占用了上万个字节,因此,可以判定为内存泄漏。 

    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);
    }
    return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value);
    }
    
    String(int offset, int count, char value[]) {   //构造方法只是改变数组的偏移和长度,不是产生新的String对象,所以造成内存泄露
        this.value = value; 
        this.offset = offset; 
        this.count = count; 
    } 

    构造方法只是改变数组的偏移和长度,不是产生新的String对象,引用的还是原字符串,原字符串永远不会被回收,所以造成内存泄露

    而在jdk1.7之后:

    public String substring(int beginIndex) {
            if (beginIndex < 0) {
                throw new StringIndexOutOfBoundsException(beginIndex);
            }
            int subLen = value.length - beginIndex;
            if (subLen < 0) {
                throw new StringIndexOutOfBoundsException(subLen);
            }
            return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
        }
    
     public String(char value[], int offset, int count) {
            if (offset < 0) {
                throw new StringIndexOutOfBoundsException(offset);
            }
            if (count <= 0) {
                if (count < 0) {
                    throw new StringIndexOutOfBoundsException(count);
                }
                if (offset <= value.length) {
                    this.value = "".value;
                    return;
                }
            }
            // 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);  //复制创建了一个新的字符数组
        }

    可以看到,在substring()的实现中,最终是使用了String 的构造函数,生成了一个新的String,不会造成内存泄露。 

    三、字符串拼接

    concat:连接string各个字符串,是创建新的String对象(字符数组),只能接受String

    +:默认是java的String类的一种重载,将+后面的对象,转换为String类型,然后再进行字符串拼接,其实都是产生了一个新的对象,+可以接其它类型

    字符串拼接几种方式的效率:+ < contact < StringBuffer < StringBuilder

    String apple = "Apple,";
    String fruit = apple + "Pear," + "Orange"

    其实底层编译器在执行上述代码的时候会的自动引入 java.lang.StringBuilder 类,上面这个例子中,编译器会创建一个 StringBuilder 对象,用来构造最终要生成的 String,并为每一个字符串调用一次 StringBuilder 中的 append() 方法,因此上述代码一共执行了三次 append() 方法。最后调用 toString 生成最终的结果,并保存为 fruit。

    但能使用StringBuilder最好不要用 +,如再循环里 += 连接会在每次循环自动创建一次StringBuilder对象,降低效率

     

  • 相关阅读:
    Flink 源码解析 —— 深度解析 Flink 序列化机制
    Flink Metrics 源码解析
    Flink 源码解析 —— JobManager 处理 SubmitJob 的过程
    vue封装插件并发布到npm上
    vue+ivew-admin开发项目,内存占用过大解决办法
    Vue-cli 2.9 多页配置及多页面之间的跳转问题
    vuex2.0 基本使用(3) --- getter
    vuex2.0 基本使用(2) --- mutation 和 action
    vuex2.0 基本使用(1) --- state
    如何新建仓并 上传本地新的项目
  • 原文地址:https://www.cnblogs.com/pluto-yang/p/10485171.html
Copyright © 2011-2022 走看看