【源码】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对象,降低效率

 

 

posted on 2019-03-07 10:40  平平无奇杨小兵  阅读(559)  评论(0编辑  收藏  举报