String类源码阅读

作者: 今有所思 | 来源:发表于2016-12-20 10:05 被阅读95次

    1. 简介

    String表示字符串,Java中所有字符串的字面值都是String类的实例,例如“ABC”。String类字符串是常量,在定义之后不能被改变,字符串缓冲区支持可变的字符串。因为String对象是不可变的,所以可以共享它们。例如:

    String str = "abc";
    

    相当于

    char data[] = {'a', 'b', 'c'};
    String str = new String(data);
    

    这里还有一些其他使用字符串的例子:

    System.out.println("abc");
    String cde = "cde";
    System.out.println("abc" + cde);
    String c = "abc".substring(2,3);
    String d = cde.substring(1, 2);
    

    2. 定义

    String类在源码中的定义:

    public final class String implements java.io.Serializable, Comparable<String>, CharSequence 
    

    可以看出String是final类型的,表示该类不能被其他类继承,同时该类实现了三个接口:java.io.Serializable Comparable<String> CharSequence

    Strings are constant; 
    their values cannot be changed after they are created. String buffers support mutable strings.
    Because String objects are immutable they can be shared. 
    

    String字符串是常量,其值在实例创建后就不能被修改,但字符串缓冲区支持可变的字符串,因为缓冲区里面的不可变字符串对象们可以被共享(其实就是使对象的引用发生了改变)。

    3. 属性

    private final char value[];
    

    这是一个字符数组,并且是final类型,用于存储字符串内容。从fianl关键字可以看出,String的内容一旦被初始化后,其不能被修改的。

    看到这里也许会有人疑惑,String初始化以后好像可以被修改啊。比如找一个常见的例子:

    String str = "hello"; 
    str = "hi";
    

    其实这里的赋值并不是对str内容的修改,而是将str指向了新的字符串。
    另外可以明确的一点:String其实是基于字符数组char[]实现的

    private int hash; // Default to 0
    

    缓存字符串的hash Code,其默认值为 0

    private static final long serialVersionUID = -6849794470754667710L;
    private static final ObjectStreamField[] serialPersistentFields =
            new ObjectStreamField[0];
    

    因为String实现了Serializable接口,所以支持序列化和反序列化支持。Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。

    4. 构造方法

    空的构造器

    public String() {
        this.value = "".value;
    }
    

    该构造方法会创建空的字符序列。不建议采取下面的创建String对象:

    String str = new String()
    str = "sample";
    

    这样的结果显而易见,会产生了不必要的对象。

    使用字符串类型的对象来初始化

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    

    这里将直接将源String中的value和hash两个属性直接赋值给目标String。因为String一旦定义之后是不可以改变的,所以也就不用担心改变源String的值会影响到目标String的值。

    使用字符数组来构造

    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    
        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);
    }
    
    

    这里值得注意的是:当我们使用字符数组创建String的时候,会用到Arrays.copyOf方法Arrays.copyOfRange方法。这两个方法是将原有的字符数组中的内容逐一复制到String中的字符数组中,会创建一个新的字符串对象,随后修改的字符数组不影响新创建的字符串。

    使用字节数组来构建String

    在Java中,String实例中保存有一个char[]字符数组,char[]字符数组是以unicode码来存储的,String 和 char 为内存形式。

    byte是网络传输或存储的序列化形式,所以在很多传输和存储的过程中需要将byte[]数组和String进行相互转化。所以,String提供了一系列重载的构造方法来将一个字符数组转化成String,提到byte[]和String之间的相互转换就不得不关注编码问题。

    public String(byte bytes[], Charset charset)
    

    该构造方法是指通过charset来解码指定的byte数组,将其解码成unicode的char[]数组,够造成新的String。这里的bytes字节流是使用charset进行编码的,想要将他转换成unicode的char[]数组,而又保证不出现乱码,那就要指定其解码方式。

    同样使用字节数组来构造String也有很多种形式,按照是否指定解码方式分的话可以分为两种:

    public String(byte bytes[]) {
        this(bytes, 0, bytes.length);
    }
    
    public String(byte bytes[], int offset, int length) {
        checkBounds(bytes, offset, length);
        this.value = StringCoding.decode(bytes, offset, length);
    }
    

    如果我们在使用byte[]构造String的时候,使用的是下面这四种构造方法(带有charsetName或者charset参数)的一种的话,那么就会使用StringCoding.decode方法进行解码,使用的解码的字符集就是我们指定的charsetName或者charset。

    String(byte bytes[]) String(byte bytes[], int offset, int length)
    String(byte bytes[], Charset charset)
    String(byte bytes[], String charsetName)
    String(byte bytes[], int offset, int length, Charset charset)
    String(byte bytes[], int offset, int length, String charsetName)
    

    们在使用byte[]构造String的时候,如果没有指明解码使用的字符集的话,那么StringCoding的decode方法首先调用系统的默认编码格式,如果没有指定编码格式则默认使用ISO-8859-1编码格式进行编码操作。主要体现代码如下:

        static char[] decode(byte[] ba, int off, int len) {
            String csn = Charset.defaultCharset().name();
            try {
                // use charset name decode() variant which provides caching.
                return decode(csn, ba, off, len);
            } catch (UnsupportedEncodingException x) {
                warnUnsupportedCharset(csn);
            }
            try {
                return decode("ISO-8859-1", ba, off, len);
            } catch (UnsupportedEncodingException x) {
                // If this code is hit during VM initialization, MessageUtils is
                // the only way we will be able to get any kind of error message.
                MessageUtils.err("ISO-8859-1 charset not available: "
                                 + x.toString());
                // If we can not find ISO-8859-1 (a required encoding) then things
                // are seriously wrong with the installation.
                System.exit(1);
                return null;
            }
    }
    
    

    使用StringBuffer和StringBuider构造一个String

    作为String的两个“兄弟”,StringBuffer和StringBuider也可以被当做构造String的参数。

    public String(StringBuffer buffer) {
        synchronized(buffer) {
          this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
        } 
    } 
    public String(StringBuilder builder) {
         this.value = Arrays.copyOf(builder.getValue(), builder.length());
    }
    

    当然,这两个构造方法是很少用到的,因为当我们有了StringBuffer或者StringBuilfer对象之后可以直接使用他们的toString方法来得到String。

    关于效率问题,Java的官方文档有提到说使用StringBuilder的toString方法会更快一些,原因是StringBuffer的toString方法是synchronized的,在牺牲了效率的情况下保证了线程安全。

    StringBuilder的toString()方法:

    @Override
    public String toString(){
        //Createacopy,don'tsharethearray
        return new String(value,0,count);
    }
    

    StringBuffer的toString()方法:

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

    一个特殊的保护类型的构造方法

    String除了提供了很多公有的供程序员使用的构造方法以外,还提供了一个保护类型的构造方法(Java 8),我们看一下他是怎么样的:

    /*
    * Package private constructor which shares value array for speed.
    * this constructor is always expected to be called with share==true.
    * a separate constructor is needed because we already have a public
    * String(char[]) constructor that makes a copy of the given char[].
    */
    String(char[] value, boolean share) {
        // assert share : "unshared not supported";
        this.value = value;
    }
    

    从代码中我们可以看出,该方法和String(char[] value)有两点区别:

    1. 该方法多了一个参数:boolean share,其实这个参数在方法体中根本没被使用。注释说目前不支持false,只使用true。那可以断定,加入这个share的只是为了区分于String(char[] value)方法,不加这个参数就没办法定义这个函数,只有参数才能进行重载
    2. 具体的方法实现不同。我们前面提到过,String(char[] value)方法在创建String的时候会用到Arrays的copyOf方法将value中的内容逐一复制到String当中,而这个String(char[] value, boolean share)方法则是直接将value的引用赋值给String的value。那么也就是说,这个方法构造出来的String和参数传过来的char[] value共享同一个数组。

    为什么Java会提供这样一个方法呢?

    性能好

    这个很简单,一个是直接给数组赋值(相当于直接将String的value的指针指向char[]数组),一个是逐一拷贝。当然是直接赋值快了。

    节约内存

    该方法之所以设置为protected,是因为一旦该方法设置为公有,在外面可以访问的话,如果构造方法没有对arr进行拷贝,那么其他人就可以在字符串外部修改该数组,由于它们引用的是同一个数组,因此对arr的修改就相当于修改了字符串,那就破坏了字符串的不可变性。

    安全的

    对于调用他的方法来说,由于无论是原字符串还是新字符串,其value数组本身都是String对象的私有属性,从外部是无法访问的,因此对两个字符串来说都很安全。

    Java7加入的新特性

    在Java 7 之前有很多String里面的方法都使用上面说的那种“性能好的、节约内存的、安全”的构造函数。
    比如:substring replace concat valueOf等方法

    实际上他们使用的是public String(char[], ture)方法来实现。

    但是在Java 7中,substring已经不再使用这种“优秀”的方法了

        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);
        }
    

    为什么呢?
    虽然这种方法有很多优点,但是他有一个致命的缺点,对于sun公司的程序员来说是一个零容忍的bug,那就是他很有可能造成内存泄露

    看一个例子,假设一个方法从某个地方(文件、数据库或网络)取得了一个很长的字符串,然后对其进行解析并提取其中的一小段内容,这种情况经常发生在网页抓取或进行日志分析的时候。
    下面是示例代码。

    String aLongString = "...a very long string..."; 
    String aPart = data.substring(20, 40);
    return aPart;
    

    在这里aLongString只是临时的,真正有用的是aPart,其长度只有20个字符,但是它的内部数组却是从aLongString那里共享的,因此虽然aLongString本身可以被回收,但它的内部数组却不能释放。
    这就导致了内存泄漏。如果一个程序中这种情况经常发生有可能会导致严重的后果,如内存溢出或性能下降。

    新的实现虽然损失了性能,而且浪费了一些存储空间,但却保证了字符串的内部数组可以和字符串对象一起被回收,从而防止发生内存泄漏,因此新的substring比原来的更健壮。

    其他方法

    length() 返回字符串长度

    public int length() {
        return value.length;
    }
    

    isEmpty() 返回字符串是否为空

    public boolean isEmpty() {
        return value.length == 0;
    }
    

    charAt(int index) 返回字符串中第(index+1)个字符(数组索引)

        public char charAt(int index) {
            if ((index < 0) || (index >= value.length)) {
                throw new StringIndexOutOfBoundsException(index);
            }
            return value[index];
    }
    
    • char[] toCharArray()转化成字符数组
    • trim()去掉两端空格
    • toUpperCase()转化为大写
    • toLowerCase()转化为小写

    需要注意

    String concat(String str) //拼接字符串
    String replace(char oldChar, char newChar) //将字符串中的oldChar字符换成newChar字符
    

    以上两个方法都使用了String(char[] value, boolean share);concat方法和replace方法不会导致元数组中有大量空间不被使用,因为他们一个是拼接字符串,一个是替换字符串内容,不会将字符数组的长度变得很短,所以使用了共享的char[]字符数组来优化。

    • boolean matches(String regex) 判断字符串是否匹配给定的regex正则表达式
    • boolean contains(CharSequence s) 判断字符串是否包含字符序列s
    • String[] split(String regex, int limit) 按照字符regex将字符串分成limit份
    • String[] split(String regex) 按照字符regex将字符串分段

    getBytes

    在创建String的时候,可以使用byte[]数组,将一个字节数组转换成字符串,同样,我们可以将一个字符串转换成字节数组,那么String提供了很多重载的getBytes方法。

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

    但是,值得注意的是,在使用这些方法的时候一定要注意编码问题。比如:

    String s = "你好,世界!"; 
    byte[] bytes = s.getBytes();
    

    在中文操作系统中可能会使用GBK或者GB2312进行编码,在英文操作系统中有可能使用iso-8859-1进行编码。这样写出来的代码就和机器环境有很强的关联性了,为了避免不必要的麻烦,要指定编码方式。

    public byte[] getBytes(Charset charset) {
        if (charset == null) throw new NullPointerException();
        return StringCoding.encode(charset, value, 0, value.length);
    }
    

    比较方法

    • boolean equals(Object anObject);

    • boolean contentEquals(StringBuffer sb);

    • boolean contentEquals(CharSequence cs);

    • boolean equalsIgnoreCase(String anotherString);

    • int compareTo(String anotherString);

    • int compareToIgnoreCase(String str);

    • boolean regionMatches(inttoffset,Stringother,intooffset,intlen) 局部匹配

    • boolean regionMatches(boolean ignoreCase,int toffset,Stringother,int ooffset,int len) 局部匹配

    字符串有一系列方法用于比较两个字符串的关系。

    前四个返回boolean的方法很容易理解,前三个比较就是比较String和要比较的目标对象的字符数组的内容,一样就返回true,不一样就返回false,核心代码如下:

    int n = value.length; 
    while (n-- != 0) {
       if (v1[i] != v2[i])
            return false;
        i++;
    }
    

    v1 v2分别代表String的字符数组和目标对象的字符数组。 第四个和前三个唯一的区别就是他会将两个字符数组的内容都使用toUpperCase方法转换成大写再进行比较,以此来忽略大小写进行比较。相同则返回true,不想同则返回false

    equals方法

       public boolean equals(Object anObject) {
            if (this == anObject) {
                return true;
            }
            if (anObject instanceof String) {
                String anotherString = (String)anObject;
                int n = value.length;
                if (n == anotherString.value.length) {
                    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;
    }
    

    该方法首先判断this == anObject ?,也就是说判断要比较的对象和当前对象是不是同一个对象,如果是直接返回true,如不是再继续比较,然后在判断anObject是不是String
    类型的,如果不是,直接返回false,如果是再继续比较,到了能终于比较字符数组的时候,他还是先比较了两个数组的长度,不一样直接返回false,一样再逐一比较值。 虽然代码写的内容比较多,但是可以很大程度上提高比较的效率。值得学习!!!

    contentEquals有两个重载:

    StringBuffer需要考虑线程安全问题,加锁之后再调用contentEquals((CharSequence) sb)方法。
    contentEquals((CharSequence) sb)则分两种情况,一种是cs instanceof AbstractStringBuilder,另外一种是参数是String类型。具体比较方式几乎和equals方法类似,先做“宏观”比较,在做“微观”比较。

    下面这个是equalsIgnoreCase代码的实现:

    public boolean equalsIgnoreCase(String anotherString) {
        return (this == anotherString) ? true
                : (anotherString != null)
                && (anotherString.value.length == value.length)
                && regionMatches(true, 0, anotherString, 0, value.length);
    }
    

    看到这段代码,眼前为之一亮。使用一个三目运算符和&&操作代替了多个if语句。

    hashCode

        public int hashCode() {
            int h = hash;
            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;
        }
    

    hashCode的实现其实就是使用数学公式:s[0]31^(n-1) + s[1]31^(n-2) + ... + s[n-1]

    所谓“冲突”,就是在存储数据计算hash地址的时候,我们希望尽量减少有同样的hash地址。如果使用相同hash地址的数据过多,那么这些数据所组成的hash链就更长,从而降低了查询效率。
    所以在选择系数的时候要选择尽量长的系数并且让乘法尽量不要溢出的系数,因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。

    现在很多虚拟机里面都有做相关优化,使用31的原因可能是为了更好的分配hash地址,并且31只占用5bits。
    在Java中,整型数是32位的,也就是说最多有2^32= 4294967296个整数,将任意一个字符串,经过hashCode计算之后,得到的整数应该在这4294967296数之中。那么,最多有 4294967297个不同的字符串作hashCode之后,肯定有两个结果是一样的。

    hashCode可以保证相同的字符串的hash值肯定相同,但是hash值相同并不一定是value值就相同。

    substring

    前面我们介绍过,java 7 中的substring方法使用String(value, beginIndex, subLen)方法创建一个新的String并返回,这个方法会将原来的char[]中的值逐一复制到新的String中,两个数组并不是共享的,虽然这样做损失一些性能,但是有效地避免了内存泄露。

    replaceFirst、replaceAll、replace区别

    • String replaceFirst(String regex,String replacement)
    • String replaceAll(String regex,String replacement)
    • String replace(CharSequence target,CharSequence replacement)
    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;
    }
    
    

    replace的参数是char和CharSequence,即可以支持字符的替换,也支持字符串的替换。

    replaceAll和replaceFirst的参数是regex,即基于规则表达式的替换。
    比如可以通过replaceAll(“\d”, “*”)把一个字符串所有的数字字符都换成星号;

    相同点是都是全部替换,即把源字符串中的某一字符或字符串全部换成指定的字符或字符串,如果只想替换第一次出现的,可以使用 replaceFirst(),这个方法也是基于规则表达式的替换。另外,如果replaceAll()和replaceFirst()所用的参数据不是基于规则表达式的,则与replace()替换字符串的效果是一样的,即这两者也支持字符串的操作。

    copyValueOf 和 valueOf

    String的底层是由char[]实现的,早期的String构造器的实现呢,不会拷贝数组的,直接将参数的char[]数组作为String的value属性。字符数组将导致字符串的变化。

    为了避免这个问题,提供了copyValueOf方法,每次都拷贝成新的字符数组来构造新的String对象。

    现在的String对象,在构造器中就通过拷贝新数组实现了,所以这两个方面在本质上已经没区别了。
    valueOf()有很多种形式的重载,包括:

     public static String valueOf(boolean b) {
           return b ? "true" : "false";
     } 
    
    public static String valueOf(char c) {
           char data[] = {c};
           return new String(data, true);
     }
    
     public static String valueOf(int i) {
           return Integer.toString(i);
     }
    
     public static String valueOf(long l) {
           return Long.toString(l);
     }
    
     public static String valueOf(float f) {
           return Float.toString(f);
     } 
    
    public static String valueOf(double d) {
         return Double.toString(d);
    }
    

    可以看到这些方法可以将六种基本数据类型的变量转换成String类型。

    intern()方法

    public native String intern(); 该方法返回一个字符串对象的内部化引用。

    String类维护一个初始为空的字符串的对象池,当intern方法被调用时,如果对象池中已经包含这一个相等的字符串对象则返回对象池中的实例,否则添加字符串到对象池并返回该字符串的引用。

    String对“+”的重载

    我们知道,Java是不支持重载运算符,String的“+”是java中唯一的一个重载运算符,那么java使如何实现这个加号的呢?我们先看一段代码:

    public static void main(String[] args) {
         String string="hollis";
         String string2 = string + "chuang";
    }
    

    然后我们将这段代码的实际执行情况:

    public static void main(String args[]){
         String string = "hollis";
         String string2 = (new         
         StringBuilder(String.valueOf(string))).append("chuang").toString();
    }
    

    看了反编译之后的代码我们发现,其实String对“+”的支持其实就是使用了StringBuilder以及他的append、toString两个方法。

    String.valueOf和Integer.toString的区别

    接下来我们看以下这段代码,我们有三种方式将一个int类型的变量变成呢过String类型,那么他们有什么区别?

    int i = 5;
    String i1 = "" + i;
    String i2 = String.valueOf(i);
    String i3 = Integer.toString(i);
    

    第三行和第四行没有任何区别,因为String.valueOf(i)也是调用Integer.toString(i)来实现的。
    第二行代码其实是String i1 = (new StringBuilder()).append(i).toString();

    首先创建了一个StringBuilder对象,然后再调用append方法,再调用toString方法。

    相关文章

      网友评论

      • a9e821f32f45:不是说数组用final修饰后,不是数组的值可以改变,而是数组的地址不可以改变了呢,String底层是一个final char value[],为啥可以修改地址而不可以改变值呢?
        今有所思:final修饰后确实可以改变数组的值,但不可以引用新的数组。
        “String底层是一个final char value[],为啥可以修改地址而不可以改变值呢?”这句话我不是很懂,我的理解是String对象被修改了之后就会重新生成一个新的String类型,并不是原来的。
      • 消失不见的影子:我想问下intern()方法返回的字符串引用有什么用途?
        今有所思:节省内存空间啊,如果已经存在了,就直接使用。

      本文标题:String类源码阅读

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