Java String源码解析

Posted StubbornAnt

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java String源码解析相关的知识,希望对你有一定的参考价值。

String类概要

  • 所有的字符串字面量都属于String类,String对象创建后不可改变,因此可以缓存共享,StringBuilder,StringBuffer是可变的实现
  • String类提供了操作字符序列中单个字符的方法,比如有比较字符串,搜索字符串等
  • Java语言提供了对字符串连接运算符的特别支持(+),该符号也可用于将其他类型转换成字符串。
  • 字符串的连接实际上是通过StringBuffer或者StringBuilder的append()方法来实现的
  • 一般情况下,传递一个空参数在这类构造函数或方法会导致NullPointerException异常被抛出。
  • String表示一个字符串通过UTF-16(unicode)格式,补充字符通过代理对表示。索引值参考字符编码单元,所以补充字符在String中占两个位置。

String是不可变的

  • String是常量,一旦被创建就不可被改变,因此可以用来共享

从String的怪异现象讲起

String是否相等

==判断的是对象的内存起始地址是否相同,equals判断自定义的语义是否相同

  • JVM为了提高内存效率,将所有不可变的字符串缓存在常量池中,当有新的不可变的字符串需要创建时,如果常量池中存在相等的字符串就直接将引用指向已有的字符串常量,而不会创建新对象
  • new创建的对象存储在堆内存,不可能与常量区的对象具有相同地址
  • 直接用字面量初始化String要比用new 关键字创建String对象效率更高
public class Demo 
    public static void main(String[] args) throws Exception 
        String s = "abc";
        String s1 = "abc";
        String s2 = "a" + "bc";
        final String str1 = "a";
        final String str2 = "bc";
        String s3 = str1 + str2;
        String s4 = new String("abc");
        System.out.println(s == s1);
        System.out.println(s == s2);
        System.out.println(s == s3);
        System.out.println(s == s4);
    
 //结果:true    true    true    false

为什么String不可变

final修饰变量,如果是基本类型那么内容运行期间不可变,如果是引用类型那么引用的对象(包括数组)运行期地址不可变,但是对象(数组)的内容是可以改变的

  • final只是保证value不会指向其他的数组,但不保证数组内容不可修改
  • private属性保证了不可以在类外访问数组,也就不能改变其内容
  • String内部没有改变value内容的函数,所以String就不可变了
  • String声明为final杜绝了通过继承的方法添加新的函数
  • 基于数组的构造方法,会拷贝数组元素,从而避免了通过外部引用修改value的情况
  • 用String构造其他可变对象时,涉及的数组只是返回的数组的拷贝而不是原数组,例如 new StringBuilder(str),会把str数组进行拷贝后传递给StringBuilder而不是传递原数组

当然只要类库设计人愿意,只要增加一个类似的setCharAt(index)的接口,String就变成可变的了

    private final char value[];
    private int hash; // Default to 0  
    public String(char value[]) 
        this.value = Arrays.copyOf(value, value.length);
      

通过反射改变String

  • final 只在编译器有效,在运行期间无效,因此可以通过反射改变value引用的对象
  • s与str始终具有相同的内存地址,反射改变了s的内容,并没有新创建对象
  • s 与 s1对应常量池中的两个对象,所以即便通过反射修改了s的内容,他们两个的内存地址还是不同的
public class Demo 
    public static void main(String[] args) throws Exception 
        String s = "abc";
        String str = s;
        String s1 = "bbb";
        System.out.println(str == s);
        Field f = s.getClass().getDeclaredField("value");
        f.setAccessible(true);
        f.set(s, new char[]'b', 'b', 'b');
        System.out.println(str + "    " + s);
        System.out.println(s == str);
        System.out.println(s == s1);
    
  //结果:bbb    bbb    true    false

String的HashCode

s的内容改变了但是hashCode值并没有改变,虽然s与s1的内容是相同的但是他们hashCode值并不相同

  • Object的hashCode方法返回的是16进制内存地址,String类重写了hashCode的,hashCode值的计算是基于字符串内容的
  • String的hashCode值初始为0,由于String是不可变的,当第一次运行完hashCode方法后String类对HashCode值进行了缓存,下一次在调用时直接返回hash值
public class Demo 
    public static void main(String[] args) throws Exception 
        String s = "abc";
        String s1 = "bbb";
        System.out.println(s.hashCode());
        Field f = s.getClass().getDeclaredField("value");
        f.setAccessible(true);
        f.set(s, new char[]'b', 'b', 'b');
        System.out.println(s + "    "+ s1);
        System.out.println(s.hashCode() +" " +s1.hashCode());
    
  //结果:96354    bbb    bbb    96354 97314

String 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;
      

toString方法中的this

  • Java为String类重载了“+”操作符,String类与其他类对象进行连接时会调用其他类的toString方法
  • 如果在其他类的toString方法中用“+”对this进行连接就会出现无限递归调用而出现栈溢出错误
  • 解决方法将this换做super.this
public class Demo 
    @Override
    public String toString() 
        //会造成递归调用
//        return "address"+super.toString();
        return "address"+super.toString();
    
    public static void main(String[] args) 
        System.out.println(new Demo());
    
  

CodePoints与CodeUnit

String的length表示的是代码单元的个数,而不是字符的个数

  • codePoints是代码点, 表示的是例如’A’, ‘王’ 这种字符,每种字符都有一个唯一的数字编号,这个数字编号就叫unicode code point。目前code point的数值范围是0~0x10FFFF。
  • codeUnit是代码单元, 它根据编码不同而不同, 可以理解为是字符编码的基本单元,java中的char是两个字节, 也就是16位的。这样也反映了一个char只能表示从u+0000~u+FFFF范围的unicode字符, 在这个范围的字符也叫BMP(basic Multiligual Plane ), 超出这个范围的叫增补字符,增补字符占用两个代码单元。
public class Demo 
    public static void main(String[] args) 
        String s = "\\u1D56B";
        System.out.println(s);
        System.out.println(s.length());
    
  

我们看看String是怎么处理增补字符的

  • 首先value字符数组的长度是根据代码单元来定的,每出现一个Surrogate字符数组长度在count的基础上加一
  • BMP字符直接存储,增补字符的用两个char分别存储高位和低位
    public String(int[] codePoints, 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 > codePoints.length - count) 
            throw new StringIndexOutOfBoundsException(offset + count);
        
        final int end = offset + count;
        // Pass 1: Compute precise size of char[]
        int n = count;
        for (int i = offset; i < end; i++) 
            int c = codePoints[i];
            if (Character.isBmpCodePoint(c))
                continue;
            else if (Character.isValidCodePoint(c))
                n++;
            else throw new IllegalArgumentException(Integer.toString(c));
        
        // Pass 2: Allocate and fill in char[]
        final char[] v = new char[n];
        for (int i = offset, j = 0; i < end; i++, j++) 
            int c = codePoints[i];
            if (Character.isBmpCodePoint(c))
                v[j] = (char)c;
            else
                Character.toSurrogates(c, v, j++);
        
        this.value = v;
      
    static void toSurrogates(int codePoint, char[] dst, int index) 
        // We write elements "backwards" to guarantee all-or-nothing
        dst[index+1] = lowSurrogate(codePoint);
        dst[index] = highSurrogate(codePoint);
      

源码解析

声明

  • String类可序列化,可比较,实现CharSequence接口提供了对字符的基本操作
  • String内部使用final字符数组进行存储,涉及value数组的操作都使用了拷贝数组元素的方法,保证了不能在外部修改字符数组
  • String重写了Object的hashCode函数使hash值基于字符数组内容,但是由于String缓存了hash值,所以即便通过反射改变了字符数组内容,hashhashCode返回值不会自动更新
  • serialVersionUID 用来确定类的版本是否正确,如果不是同一个类会抛出InvalidCastException异常
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence  
    private final char value[];
    private static final long serialVersionUID = -6849794470754667710L;  
    /** Cache the hash code for the string */
    private int hash; // Default to 0
    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;
      

构造函数

  • String主要提供了通过String,StringBuilder,StringBuffer,char数组,int数组(CodePoint),byte数组(需要指定编码)进行初始化
  • 当通过字符串初始化字符串时,并没有执行value数组拷贝,因为original的value数组是不可以在外部修改的,也就保证了新String对象的不可修改
  • 通过字符数组,StringBuffer,StringBuilder进行初始化时,就要执行value数组元素的拷贝,创建新数组,防止外部对value内容的改变
  • 通过byte数组进行初始化,需要指定编码,或使用默认编码(ISO-8859-1),否则无法正确解释字节内容
  • 通过Unicode代码点进行的初始化,可能会包含非BMP字符(int值大于65535),这时候字符串的长度可能会长于int数组的长度,(见本文前面增补字符处理部分)
    public String(String original) 
        this.value = original.value;
        this.hash = original.hash;
      
    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());
      
    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) 
            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);
     
    public String(byte bytes[], int offset, int length, Charset charset) 
        if (charset == null)
            throw new NullPointerException("charset");
        checkBounds(bytes, offset, length);
        this.value =  StringCoding.decode(charset, bytes, offset, length);
     
    public String(byte bytes[], int offset, int length) 
        checkBounds(bytes, offset, length);
        this.value = StringCoding.decode(bytes, offset, length);
      
    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;
        
     

内部构造函数

使用外部数组来初始化String内部数组只有保证传入的数组不可能被改变才能保证String的不可变性,例如用String初始化String对象时

  • 这种方法使用共享value数组的方法避免了数组的拷贝,提高了效率
  • 上面分析指出如果直接使用外部传入的数组不能保证String的不可变性,这个方法只在String的内部使用,不能由外部调用
  • 添加share参数,只是为了重载构造函数,share必须为true
  • 该函数只用在不能缩短String长度的函数中,如concat(str1,str2),如果用在缩短String长度的函数如subString中会造成内存泄漏
    String(char[] value, boolean share) 
        // assert share : "unshared not supported";
        this.value = value;
      
    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);
      
        // 使用了Arrays.copyof方法来构造新的数组,拷贝元素,而不是共用数组
    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);
      

如果String(value,share)可以在外部使用,就可以改变字符串内容

public class Demo 
    public static void main(String[] args) 
        char[] arr = new char[] 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd';
        String s = new String(arr,true);
        arr[0] = 'a';
        System.out.println(s);
    
 

aLongString 已经不用了,但是由于其与aPart共享value数组,所以不能被回收,造成内存泄漏

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

主要方法

其他主要方法

length() 返回字符串长度

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

charAt(int index) 返回字符串中第(index+1)个字符

char[] toCharArray() 转化成字符数组

trim() 去掉两端空格

toUpperCase() 转化为大写

toLowerCase() 转化为小写

String concat(String str) //拼接字符串

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

//以上两个方法都使用了String(char[] value, boolean share);

boolean matches(String regex) //判断字符串是否匹配给定的regex正则表达式

boolean contains(CharSequence s) //判断字符串是否包含字符序列s

String[] split(String regex, int limit) 按照字符regex将字符串分成limit份。

String[] split(String regex)

重载的valueOf方法

可以看到主要是调用构造函数或者是调用对应类型的toString完成到字符串的转换

    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);
      
    public static String valueOf(char data[], int offset, int count) 
        return new String(data, offset, count);
     
    public static String copyValueOf(char data[], int offset, int count) 
        // All public String constructors now copy the data.
        return new String(data, offset, count);
     

字符串查找算法 indexOf

可以看到String的字符串匹配算法使用的是朴素的匹配算法,即前向匹配,当遇到不匹配字符时,主串从下一个字符开始,字串从开始位置开始
其他相关字符串匹配算法

    static int indexOf(char[] source, int sourceOffset, int sourceCount,
            char[] target, int targetOffset, int targetCount,
            int fromIndex) 
        if (fromIndex >= sourceCount) 
            return (targetCount == 0 ? sourceCount : -1);
        
        if (fromIndex < 0) 
            fromIndex = 0;
        
        if (targetCount == 0) 
            return fromIndex;
        
        char first = target[targetOffset];
        int max = sourceOffset + (sourceCount - targetCount);
        for (int i = sourceOffset + fromIndex; i <= max; i++) 
            /* Look for first character. */
            if (source[i] != first) 
                while (++i <= max && source[i] != first);
            
            /* Found first character, now look at the rest of v2 */
            if (i <= max) 
                int j = i + 1;
                int end = j + targetCount - 1;
                for (int k = targetOffset + 1; j < end && source[j]
                        == target[k]; j++, k++);
                if (j == end) 
                    /* Found whole string. */
                    return i - sourceOffset;
                
            
        
        return -1;
      

编码问题 getBytes

  • 字符串最终都是使用机器码以字节存储的,当我们将字符串转换为字节的时候也需要给定编码,同一个字符不同的编码就对应不同的字节
  • 如不指定编码,就会使用默认的编码ISO-8859-1进行编码
  • 编码时为了避免平台编码的干扰,应当指定确定的编码
    String s = "你好,世界!";
    byte[] bytes = s.getBytes("utf-8");  

    public byte[] getBytes(String charsetName)
            throws UnsupportedEncodingException 
        if (charsetName == null) throw new NullPointerException();
        return StringCoding.encode(charsetName, value, 0, value.length);
     
    static byte[] encode(String charsetName, char[] ca, int off, int len)
        throws UnsupportedEncodingException
    
        StringEncoder se = deref(encoder);
        String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
        if ((se == null) || !(csn.equals(se.requestedCharsetName())
                              || csn.equals(se.charsetName()))) 
            se = null;
            try 
                Charset cs = lookupCharset(csn);
                if (cs != null)
                    se = new StringEncoder(cs, csn);
             catch (IllegalCharsetNameException x) 
            if (se == null)
                throw new UnsupportedEncodingException (csn);
            set(encoder, se);
        
        return se.encode(ca, off, len);
     

比较方法

  • 所有比较方法都是比较对应的字符数组的内容,后两个比较方法用来进行区段比较
  • 在进行数组比较时,如果可以通过长度进行初步判断,一般可以提高效率
    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(int toffset, String other, int ooffset,int len)  //局部匹配
    boolean regionMatches(boolean ignoreCase, int toffset,String other, int ooffset, int len)   //局部匹配  

    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;
      

替换函数 replace

  • 单字符替换会替换所有特定字符的出现
  • replace为普通(literal)替换,不用正则表达式
  • replaceFirst与replaceAll都使用了正则表达式
    public String replace(CharSequence target, CharSequence replacement) 
        return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
                this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
     
    public String replaceFirst(String regex, String replacement) 
        return Pattern.compile(regex).matcher(this).replaceFirst(replacement);
    
    public String replaceAll(String regex, String replacement) 
        return Pattern.compile(regex).matcher(this).replaceAll(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;
    

常量池相关方法

  • 每当定义一个字符串字面量,字面量进行字符串连接,或者final的String字面量初始化的变量的连接的变量时都会检查常量池中是否有对应的字符串,如果有就不创建新的字符串,而是返回指向常量池对应字符串的引用
  • 所有通过new String(str)方式创建的对象都会存在与堆区,而非常量区
  • 普通变量的连接,由于不能在编译期确定下来,所以不会存储在常量区
public native String intern();  

运算符的重载

  • String对“+”运算符进行了重载,通过反编译我们看到重载是通过StringBuilder的append方法,及String的valueOf方法实现的
  • int值转String过程中(”“+i)这种方法实际为(new StringBuilder()).append(i).toString();,而另外两种都是调用Integer的静态方法Integer.toString完成
// int转String的方法比较
public class Demo 
    public static void main(String[] args) throws Exception 
        int i = 5;
        String i1 = "" + i;
        String i2 = String.valueOf(i);
        String i3 = Integer.toString(i);
    
 
// 原始代码
public class Demo 
    public static void main(String[] args) throws Exception 
        String string="hollis";
        String string2 = string + "chuang";
    
 
//反编译代码
public class Demo 
    public static void main(String[] args) throws Exception 
        String string = "hollis";
        String string2 = (new StringBuilder(String.valueOf(string))).append("chuang").toString();
    

以上是关于Java String源码解析的主要内容,如果未能解决你的问题,请参考以下文章

Java String源码解析

java源码解析之String类

Java面试题—Javac编译器

java源码解析之String类

Spring源码解析:父子容器的概念

Java并发编程22Exchanger源码解析