String部分源码解析和相关知识(JDK1.8)

String部分源码解析(JDK1.8):

1.底层结构:

​ 以主流的JDK1.8版本来说,String内部实际存储结构为final关键字修饰的char数组,源码如下:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    //用于存储字符串的值
    private final char value[];
	
    /** Cache the hash code for the string */
    //缓存该字符串的hashcode
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    //使用JDK 1.0.2中的serialVersionUID进行互操作
    private static final long serialVersionUID = -6849794470754667710L;

   

小结:

(1)String是一个final类,说明Sting不能被继承。设计为final类的好处:第一是安全,第二个好处是高效。只有字符串是不可变的类时,我们才能实现字符串常量池(JDK1.7时从方法区移入到堆中了),字符串常量池可以为我们缓存字符串,提高程序的运行效率。

(2)String中保存数据的是一个final修饰的char数组value,也就是说 value 一旦被赋值,内存地址是绝对无法修改的,而且 value 的权限是 private 的,外部绝对访问不到,String 也没有开放出可以对 value 进行赋值的方法,所以说 value 一旦产生,内存地址就根本无法被修改。

以上两点就是String不可变的原因。

2.构造方法:4个比较重要的构造方法

// String 为参数的构造方法
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}
// char[] 为参数构造方法
public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}
// StringBuffer 为参数的构造方法
public String(StringBuffer buffer) {
    synchronized(buffer) {
        this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
    }
}
// StringBuilder 为参数的构造方法
public String(StringBuilder builder) {
    this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
//.....还有很多

3.重要方法:

(1)equals方法:比较两个字符串是否相等(值)

public boolean equals(Object anObject) {
	//1.先判断两个字符串对象的内存地址是否相同,相同返回true
    if (this == anObject) {
        return true;
    }
    //2.判断待比较对象是否为String类型,不是返回false
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        //3.该对象为String类型的话,就判断他们的长度是否相同,不相同就不需要继续比较了
        if (n == anotherString.value.length) {
       //4.长度相同,就把两个字符串对象的值转化为char数组,然后进行逐个比较!若有一个不等就返回false
            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;
}

体会:从 equals 的源码可以看出,逻辑非常清晰,完全是根据 String 底层的结构来编写出相等的代码。这也提供了一种思路给我们:如果有人问如何判断两者是否相等时,我们可以从两者的底层结构出发,这样可以迅速想到一种贴合实际的思路和方法,就像 String 底层的数据结构是 char 的数组一样,判断相等时,就挨个比较 char 数组中的字符是否相等即可。

(2)compareTo方法:比较两个字符串,返回两个字符串第一个不同字符的下标

public int compareTo(String anotherString) {
    int len1 = value.length;
    int len2 = anotherString.value.length;
    //1.获取到两个字符串的最小长度
    int lim = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;
    int k = 0;
    //2.对比每一个字符
    while (k < lim) {
        char c1 = v1[k];
        char c2 = v2[k];
        //3.有字符不相等就返回第一个不相同字符ASCII码的差值
        if (c1 != c2) {
            return c1 - c2;
        }
        k++;
    }
    //4.否则就返回两个字符串长度的差值
    return len1 - len2;
}

当compareTo返回0时,则表示两个字符串的值相同。

(3)subString方法:该方法主要是为了截取字符串连续的一部分

substring 有两个方法:

  1. public String substring(int beginIndex, int endIndex) beginIndex:开始位置,endIndex:结束位置;
  2. public String substring(int beginIndex)beginIndex:开始位置,结束位置为文本末尾。

substring 方法的底层使用的是字符数组范围截取的方法 :Arrays.copyOfRange(字符数组, 开始位置, 结束位置); 从字符数组中进行一段范围的拷贝。

public String substring(int beginIndex, int endIndex) {
	//先检查beginIndex和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);
    }
    //subtring底层其实是调用了String的一个构造函数
    return ((beginIndex == 0) && (endIndex == value.length)) ? 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);
        }
        
        //其实没什么特别的,就是调用了Arrays.copyOfRange()方法,从字符数组中进行一段范围的拷贝。
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }


(4)replace方法:

替换在工作中也经常使用,有 replace 替换所有字符、replaceAll 批量替换字符串、replaceFirst 替换遇到的第一个字符串三种场景。

其中在使用 replace 时需要注意,replace 有两个方法,一个入参是 char,一个入参是 String,前者表示替换所有字符,如:name.replace('a','b'),后者表示替换所有字符串,如:name.replace("a","b"),两者就是单引号和多引号的区别。

需要注意的是, replace 并不只是替换一个,是替换所有匹配到的字符或字符串哦

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

(5)split方法:

​ 拆分我们使用 split 方法,该方法有两个入参数。第一个参数是我们拆分的标准字符,第二个参数是一个 int 值,叫 limit,来限制我们需要拆分成几个元素。如果 limit 比实际能拆分的个数小,按照 limit 的个数进行拆分,我们演示一个 demo:

String s =“boo:and:foo”;
// 我们对 s 进行了各种拆分,演示的代码和结果是:
s.split(" ;//结果:[“boo”,“and”,“foo”]
s.split(":",2) ;//结果:[“boo”,“and:foo”]
s.split(":",5);// 结果:[“boo”,“and”,“foo”]
s.split(":",-2) ;//结果:[“boo”,“and”,“foo”]
s.split(“o”) ;//结果:[“b”,"",":and:f"]
s.split(“o”,2) ;//结果:[“b”,“o:and:foo”]

(6)其他重要方法:

  • indexOf() :查询字符串首次出现下标的位置
  • lastIndexOf() :查询字符串最后一次出现该下标的位置
  • contains() :查询字符串中是否包含另一个字符串
  • toLowerCase():把字符串全部转换成小写
  • toUpperCase():把字符串全部转换成大
  • length():查询字符串的长度
  • trim():去掉字符串首尾空格
  • replace():替换字符串中的某些字符
  • split():把字符串分割并返回字符串数组
  • join():把字符串数组转为字符串
  • contact() :将参数字符串拼接到指定字符串后面

4.相关面试题:

(1)==和equals()方法的区别?

对于基本类型,==比较的是值;对于引用类型,==比较的是内存地址。

没有被重写的equals()方法,等价于==,比较的是两个对象的内存地址,源码如下

public boolean equals(Object obj) {
    return (this == obj);
}

被重写过的euquls方法,例如String的euqals()方法,比较的是对象的值

(2)为什么要用final修饰Sting?final修饰的好处?

第一:不可变,安全。

第二:高效。只有字符串是不可变时,我们才能实现字符串常量池,字符串常量池可以为我们提高程序运行的效率。

(3)String 、SringBuffer 、StringBuilder的区别?

①可变性:

​ String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以 String 对象是不可变的。而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。

②线程安全性

​ String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

③性能

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  1. 操作少量的数据: 适用 String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

(4)String的intern()方法?

判断常量池中是否存在s,若存在则返回常量池中的引用,s的指向不会发生更改;如果常量池中不存在该字符串,那么就新建一个这样的字符串放到常量池中。**

返回值:String

使用常量池的方法一个是通过双引号定义字符串例如:String S = “1”;还有就是上面的intern方法。

(5)String和JVM?

​ String 常见的创建方式有两种,new String() 的方式和直接赋值的方式,直接赋值的方式会先去字符串常量池中查找是否已经有此值,如果有则把引用地址直接指向此值,否则会先在常量池中创建,然后再把引用指向此值;而 new String() 的方式一定会先在堆上创建一个字符串对象,然后再去常量池中查询此字符串的值是否已经存在,如果不存在会先在常量池中创建此字符串,然后把引用的值指向此字符串

​ 小贴士:JDK 1.7 之后把永生代换成的元空间,把字符串常量池从方法区移到了 Java 堆上。

学习参考:https://www.imooc.com/read/47/article/844

你可能感兴趣的:(Java源码解析)