String源码解析

假如想象 提交于 2019-11-30 22:45:57

定义

先看一下文档中的注释

12345
 * 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对象是常量,创建之后就不能被修改,所以该对象可以被多线程共享。

12345678910111213
public final class     implements java.io.Serializable, Comparable<>, CharSequence {        private final char value[];    private int hash; // Default to 0    private static final long serialVersionUID = -6849794470754667710L;    private static final ObjectStreamField[] serialPersistentFields =        new ObjectStreamField[0];    ......}

从源码中可以看出,String是被final修饰的,说明该类不能被继承。并且实现了CharSequence, Comparable, Serializable接口。Serializable接口用于实现String的序列化和反序列化操作;Comparable接口用于实现字符串的比较操作;CharSequence是字符串类的父接口,StringBuffer和StringBuilder都继承自该接口。

  1. value字段是实现String类的底层数组,用于存储字符串内容。final修饰基本数据类型,那么在运行期间其内容不可变,如果修饰的是引用类型,那么引用的对象(包括数组)运行期地址不可变,但是对象的内容是可以改变的。
  2. hash字段用于缓存String对象的hash值,防止多次计算hash造成的时间损耗。
  3. 因为String实现了Serializable接口,所以需要serialVersionUID字段用来在String反序列化时,通过对比字节流中的serialVersionUID和本地实体类中的serialVersionUID是否一致,如果相同就可以进行反序列化,否则就会抛出InvalidCastException异常。

构造方法

空参构造方法

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

该构造方法会创建一个空的字符序列,因为字符串的不可变对象,之后对象的赋值会指向新的字符串,因此使用这种构造方法会多创建一个无用对象。

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

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

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

使用字符数组初始化

12345678910
// 参数为char数组,通过java.utils包中的Arrays.copyOf复制public (char value[]) {    this.value = Arrays.copyOf(value, value.length);}// 使用字符数组的一部分初始化,通过Arrays.copyOfRange复制public String(char value[], int offset, int count) {    // 异常检测  	......    this.value = Arrays.copyOfRange(value, offset, offset+count);}

使用字节数组初始化

在Java中,String实例保存有一个char[]字符数组,char[]字符数组是以Unicode编码方式存储的,String和char为内存形式,byte是网络传输或存储的序列化形式,所以在很多传输和存储过程中需要将byte[]数组和String进行相互转化。字节和字符自检的转化需要指定编码,不然很可能会出现乱码。String提供了多种字节数组的重载构造函数:

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

如果我们在使用 byte[] 构造 String 的时候,如果指定了charsetName或者charset参数的话,那么就会使用 StringCoding.decode 方法进行解码,使用的解码的字符集就是我们指定的 charsetName 或者 charset。如果没有指定解码使用的字符集的话,那么StringCodingdecode方法首先会使用系统的默认编码格式(ISO-8859-1)。

使用StringBuffer和StringBuilder初始化

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

因为StringBuilder不是线程安全的,所以在初始化时不需要加锁;而StringBuilder则需要加锁。我们一般使用StringBuffer和StringBuilder的toString方法来获取String,而很少使用String的这两种构造方法。

特殊的构造方法

String除了提供了很多共有的构造方法,还提供了一个保护类型的构造方法:

1234
String(char[] value, boolean share) {    // assert share : "unshared not supported";    this.value = value;}

该方法和String(char[] value)有两点区别:

  • 该方法多了一个参数:boolean share,但该参数在函数中并没有使用。因此加入该参数的目的只是为了区分String(char[] value)方法,只有参数不同才能被重载。
  • 该方法直接修改了value数组的引用,也就是说共享char[] value数组。而String(char[] value)通过Arrays.copyOf将参数数组内容复制到String中。

使用这种方式的的优点很明显:

  • 性能好,直接修改指针,避免了逐一拷贝。

  • 节约内存,底层共享同一字符数组。

    当然这种方式也存在缺点,如果外部修改了传进来的字符数组的内容,由于他们引用的是同一个数组,因此外部对数组的修改相当于修改了字符串。为了保证字符串对象的不变性,将其访问权限设置成了default,其他类无法通过该构造方法初始化字符串对象。这样一来,无论源字符串还是新字符串,其value数组本身都是String对象的私有属性,从外部无法访问,保证了String的安全性。该函数只能用在不能缩短String长度的函数中,如concat(str1, str2),如果用在缩短String长度的函数如subString中会造成内存泄漏。

经典方法技巧

equals方法

123456789101112131415161718192021
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;}
  1. 先判断两个对象的地址是否相等
  2. 在判断是否是String类型
  3. 如果都是String类型,就先比较长度是否相等,然后再逐一比较值。值的比较采取了短路操作,发现不一样的就返回false

compareTo方法

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

从0开始逐一判断字符是否相等,若不相等则做差运算,巧妙的避免了三种判断情况。若字符都相等,接直接返回长度差值。所以在判断两个字符串大小时,使用是否为正数/负数/0,而不是通过1//-1/0判断。

hashCode方法

123456789101112
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;}
  1. 若第一次调用hashCode方法且value数组长度大于0,则通过算法s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]计算hash值。hash值很多时候用来判断两个对象的值是否相等,所以需要尽可能的避免冲突。选择31是因为31是一个素数,且i * 31可以通过(i << 5) - 1来提高运算速度,现在很多虚拟机都有做相关优化。 hashCode可以保证相同的字符串的hash值肯定相同,但是,hash值相同并不一定是value值就相同。
  2. 返回缓存的hash值。

replaceFirst、replaceAll,replace区别

1234
String replaceFirst(String regex, String replacement)String replaceAll(String regex, String replacement)String replace(Char Sequencetarget, Char Sequencereplacement)String replace(CharSequence target, CharSequence replacement)
  1. replace的参数是char和CharSequence,既可以支持字符的替换,也支持字符串的替换
  2. replaceFirstreplaceAll的参数是regex,基于正则表达式替换
  3. replacereplaceAll方法会替换字符串中的全部字符或字符串,replaceFirst只替换第一次出现的字符或字符串

copyValueOf和valueOf

String的底层是通过char[]实现的,早期的String构造器的实现并不会拷贝数组。为了防止char[]数组被外部修改,提供了copyValueOf方法,每次都拷贝成新的字符数组来构造新的String对象。但是现在的String在构造器中就通过拷贝新数组实现,所以这两个方法在本质上已经没区别了。

valueOf()有很多种重载形式:

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

底层调用了基本数据类型的toString()方法。

intern方法

1
public native String intern();

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

其他方法

12345678910111213141516171819202122232425262728293031323334353637383940414243
int length() //返回字符串长度boolean isEmpty() //返回字符串是否为空char charAt(int index) //返回字符串中第(index+1)个字符char[] toCharArray() //转化成字符数组void trim() //去掉两端空格String toUpperCase() //转化为大写String 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) //判断字符串是否包含字符序列sString[] split(String regex, int limit) //按照字符regex将字符串分成limit份。String[] split(String regex) //按照regex表达式切分字符串  boolean equals(Object anObject) //比较对象  boolean contentEquals(String Buffersb) //与字符串比较内容  boolean contentEquals(Char Sequencecs) //与字符比较内容  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) //可忽略大小写局部匹配

String对“+”的重载

Java不支持运算符重载,但是String可以通过+来连接两个字符串。那么java是如何实现对+的重载的呢?

123456
public class Main{  public static void main(String[] args){    String str1 = "windy";    string str2 = str1 + "lee";  }}

反编译Main.java,执行命令javap -c Main,输出结果:

我们看到了StringBuilder,还有windy和lee,以及调用了StringBuilder的appendtoString方法。既然编译器已经在底层为我们进行了优化,那么为什么还要提倡我们用StringBuilder呢?

我们注意到在第3行代码,new了一个StringBuilder对象,如果实在一个循环里面,我们使用”+”号就会创建多个StringBuilder的对象。但是编译器事先不知道我们StringBuilder的长度,并不能事先分配好缓冲区,会加大内存的开销,而且使用重载的时候根据java的内存分配也会创建多个对象。

switch对字符串支持的实现

1234567891011121314
public class Main {     public static void main(String[] args) {         String str = "world";         switch (str) {         case "hello":               System.out.println("hello");              break;         case "world":             System.out.println("world");             break;         default: break;       }    }}

反编译之后得到

123456789101112131415
public static void main(String args[]) {       String str = "world";       String s;       switch((s = str).hashCode()) {          case 99162322:               if(s.equals("hello"))                   System.out.println("hello");               break;          case 113318802:               if(s.equals("world"))                   System.out.println("world");               break;          default: break;       }  }
  1. 首先调用String的hashCode方法,拿到相应的Code,通过这个code然后给每个case唯一的标识
  2. 判断时先获取对象的hashCode,进入对应的case分支
  3. 通过equals方法进行安全检查,这个检查是必要的,因为哈希可能会发生冲突

switch只支持整型,其他数据类型都是转换成整型之后在使用switch的

总结

  • String被final修饰,一旦被创建,无法修改

    • final保证value不会指向其他的数组,但不保证数组内容不可修改
    • private属性保证不可在类外访问数组,也就不能改变其内容
    • String内部没有改变value内容的函数,保证String不可变
    • String声明为final杜绝了通过集成的方法添加新的函数
    • 基于数组的构造方法,会拷贝数组元素,避免了通过外部引用修改value的情况
    • 用String构造其他可变对象时,返回的数组的拷贝

    final只在编译期有效,在运行期间无效,因此可以通过反射改变value引用的对象。反射虽然改变了s的内容,并没有创建新的对象。而且由于String缓存了hash值,所以通过反射改变字符数组内容,hashCode返回值不会自动更新。

  • String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。

  • 如果你需要一个可修改的字符串,应该使用StringBuilder或者 StringBuffer。

  • 如果你只需要创建一个字符串,你可以使用双引号的方式,如果你需要在堆中创建一个新的对象,你可以选择构造函数的方式。

原文:大专栏  String源码解析


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!