String、StringBuilder、StringBuffer、StringJoiner源码分析

String类特点

  • 字符串内容一旦声明则不可改变(final修饰),String类对象内容的改变是依靠引用关系的变更实现的。
  • 正是因为字符串内容不可改变,所以字符串是可以共享使用的,常量池。也是线程安全的。
  • 字符串底层是final修饰的char[]数组,(JDK9之后是byte[]字节数组)
  • String类对象的相等判断使用equals() 方法完成,重写了。
  • String类有两种实例化方式,使用直接赋值可以不产生垃圾空间,并且可以自动入池,不要使用构造方法完成。

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

String中有一个字段hash来存储该串的哈希值,在第一次调用hashCode方法时,字符串的哈希值被计算并且赋值给hash字段。之后再调用hashCode方法便可以直接取hash字段返回。

String类中的hashCode计算方法还是比较简单的,就是以31为权,每一位字符的ASCII值进行计算,用自然溢出来等效取模。

哈希计算公式可以记为s[0]*31(n-1)+s[1]*31(n-2)+…+s[n-1]。

主要原因是因为31是一个奇素数,所以31i=32i-i=(i<<5)-i,这种位移与减法结合的计算相比一般的运算块很多。

String类可以被继承吗

String类底层是一个final的字节数组,类被final修饰,final修饰的类不能被继承。

String类+操作

	public static void main(String[] args) {
        String s1 = "a" + "b" + "c";  // 字面量+得到abc,存放到常量池
        String s2 = "abc"; // abc存放在常量池,直接将常量池的地址返回
        /**
         * 最终java编译成.class,再执行.class
         */
        System.out.println(s1 == s2); // true,因为存放在字符串常量池
        System.out.println(s1.equals(s2)); // true
        
        String a = "abc";
        String b = "def";
        //自JDK1.5之后,Java虚拟机执行字符串的+操作时,内部实现也是StringBuilder,之前采用StringBuffer实现。
        //+操作时,是在堆创建了对象
        String c = a + b;
        System.out.println(c == "abcdef"); //false
        System.out.println(a == "abc"); //true

        final String a1 = "abc";
        final String b1= "def";
        //a1,b1常量编译期就赋值了,常量池,a1 + b1也是编译器确定赋值给了c1,常量池
        String c1 = a1 + b1;
        System.out.println(c1 == "abcdef"); //true
        System.out.println(a1 == "abc"); //true
        
        final String a2 = "abc";
        String b2= "def";
        //也是StringBuilder
        String c2 = a2 + b2;
        System.out.println(c2 == "abcdef"); //false
        System.out.println(a2 == "abc"); //true
    }

intern()方法

JDK6中

String s = new String("1");  // 在常量池中已经有了
s.intern(); // 将该对象放入到常量池。但是调用此方法没有太多的区别,因为已经存在了1
String s2 = "1";
System.out.println(s == s2); // false, 堆vs池

String s3 = new String("1") + new String("1");
s3.intern(); // 字符串常量池没有11,则创建一个11放到字符串常量池中
String s4 = "11";
System.out.println(s3 == s4); // false

输出结果

false
false

为什么对象会不一样呢?

  • 一个是new创建的对象,一个是常量池中的对象,显然不是同一个

如果是下面这样的,那么就是true

String s = new String("1");
s = s.intern(); // 字符串常量池有1,则直接返回常量池中的1
String s2 = "1";
System.out.println(s == s2); // true

而对于下面的来说,因为 s3变量记录的地址是 new String(“11”),然后这段代码执行完以后,常量池中不存在 “11”,这是JDK6的关系,然后执行 s3.intern()后,就会在常量池中生成 “11”,最后 s4用的就是s3的地址

为什么最后输出的 s3 == s4 会为false呢?

这是因为在JDK6中创建了一个新的对象 “11”,也就是有了新的地址, s2 = 新地址

而在JDK7中,在JDK7中,并没有创新一个新对象,而是指向常量池中的新对象

JDK7/8中

String s = new String("1");
s.intern(); // 字符串常量池建立地址引用指向s
String s2 = "1";
System.out.println(s == s2); // true

String s3 = new String("1") + new String("1");
s3.intern(); // 字符串常量池建立地址引用指向s3
String s4 = "11";
System.out.println(s3 == s4); // true

JDK1.6中,将这个字符串对象尝试放入串池。

  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
  • 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址

JDK1.7起,将这个字符串对象尝试放入串池。

  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
  • 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址

JDK1.6和JDK1.7String的intern()变化本质原因是字符串常量池位置的变化,JDK7后字符串常量池是在堆中,以前是在方法区中。JDK7后,intern(),如果池中不存在该字符串,不会新建该字符串,而是在常量池建立引用指向堆中该字符串对象。所以JDK7的intern()后都是相同的对象地址,这是为了节省内存空间,因为字符串常量池已经属于堆了。JDK6则会在常量池创建新的字符串。

new String(“a”) + new String(“b”) 会创建几个对象

public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("a") + new String("b");
    }
}

字节码文件为

 0 new #2 
 3 dup
 4 invokespecial #3 >
 7 new #4 
10 dup
11 ldc #5 
13 invokespecial #6 >
16 invokevirtual #7 
19 new #4 
22 dup
23 ldc #8 
25 invokespecial #6 >
28 invokevirtual #7 
31 invokevirtual #9 
34 astore_1
35 return

我们创建了6个对象

  • 对象1:new StringBuilder()
  • 对象2:new String(“a”)
  • 对象3:常量池的 a
  • 对象4:new String(“b”)
  • 对象5:常量池的 b
  • 对象6:toString中会创建一个 new String(“ab”)
    • 调用toString方法,不会在常量池中生成ab

StringBuilder和StringBuffer的区别

StringBuilder是线程不安全的(线程同步访问的时候会出问题),但是效率相对较高。

String类型使用加号进行拼接字符串的时候,会产生很多临时字符串对象,底层是StringBuilder拼接。

StringBuffer是线程安全的。(StringBUffer只会产生一个对象)

StringBuilder和StringBuffer的内部实现跟String类一样,都是通过一个char数组存储字符串的,不同的是String类里面的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer的char数组是可变的。

StringBuilder线程不安全原理

多线程案例:

public class StringBuilderDemo {  
  
    public static void main(String[] args) throws InterruptedException {  
        StringBuilder stringBuilder = new StringBuilder();  
        for (int i = 0; i < 10; i++){  
            new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    for (int j = 0; j < 1000; j++){  
                        stringBuilder.append("a");  
                    }  
                }  
            }).start();  
        }  
        Thread.sleep(100);  
        System.out.println(stringBuilder.length());  
    }  
  
}  

这段代码创建了10个线程,每个线程循环1000次往StringBuilder对象里面append字符。正常情况下代码应该输出10000,但是实际运行会输出什么呢?

String、StringBuilder、StringBuffer、StringJoiner源码分析_第1张图片

结果小于预期的10000,并且还抛出了一个ArrayIndexOutOfBoundsException异常(异常不是必现)。

结果分析:

StringBuilder和StringBuffer都继承了AbstractStringBuilder,AbstractStringBuilder有两个成员变量

//存储字符串的具体内容  
char[] value;  
//已经使用的字符数组的数量  
int count;  

StringBuilder的append()方法调用的父类AbstractStringBuilder的append()方法

public AbstractStringBuilder append(String str) {  
    if (str == null)  
        return appendNull();  
    int len = str.length();  
    ensureCapacityInternal(count + len);  
    str.getChars(0, len, value, count);  
    count += len;  
    return this;  
}  

我们先不管代码的第五行和第六行干了什么,直接看第七行,count += len不是一个原子操作。假设这个时候count值为10,len值为1,两个线程同时执行到了第七行,拿到的count值都是10,执行完加法运算后将结果赋值给count,所以两个线程执行完后count值为11,而不是12。这就是为什么测试代码输出的值要比10000小的原因。

异常分析:

为什么会抛出ArrayIndexOutOfBoundsException异常

我们看回AbstractStringBuilder的append()方法源码的第五行,ensureCapacityInternal()方法是检查StringBuilder对象的原char数组的容量能不能盛下新的字符串,如果盛不下就调用expandCapacity()方法对char数组进行扩容。

private void ensureCapacityInternal(int minimumCapacity) {  
        // overflow-conscious code  
    if (minimumCapacity - value.length > 0)  
        expandCapacity(minimumCapacity);  
}  

void expandCapacity(int minimumCapacity) {  
    //计算新的容量  
    int newCapacity = value.length * 2 + 2;  
    ...  
    value = Arrays.copyOf(value, newCapacity);  
}  

扩容的逻辑就是new一个新的char数组,新的char数组的容量是原来char数组的两倍再加2,再通过System.arrayCopy()函数将原数组的内容复制到新数组,最后将指针指向新的char数组。

AbstractStringBuilder的append()方法源码的第六行,是将String对象(新增的字符串)里面char数组里面的内容拷贝到StringBuilder对象的char数组里面,代码如下:

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {  
    ...     
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);  
}  

拷贝流程见下图

String、StringBuilder、StringBuffer、StringJoiner源码分析_第2张图片

假设现在有两个线程同时执行了StringBuilder的append()方法,两个线程都执行完了第五行的ensureCapacityInternal()方法,此刻count=5。

String、StringBuilder、StringBuffer、StringJoiner源码分析_第3张图片

这个时候线程1的cpu时间片用完了,线程2继续执行。线程2执行完整个append()方法后count变成6了

String、StringBuilder、StringBuffer、StringJoiner源码分析_第4张图片

线程1继续执行第六行的str.getChars()方法的时候拿到的count值就是6了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常。

StringBuffer线程安全原理

    //安全是因为方法加了synchronized

	public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        //扩大char数组大小
        ensureCapacityInternal(count + len);
        //getChars方法是将此字符串中的字符复制到目标字符数组
        //-->底层是System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);是一个native方法,帮我们把新加的字符串this添加到目标字符串value后
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

StringJoiner拼接类

Java8新增辅助类,新特性之一。

StringJoiner替代StringBuilder,支持分割,可以添加分隔符、前缀、后缀。底层也是使用StringBuilder进行拼接字符串。

// 效果
StringJoiner sj = new StringJoiner(",");
IntStream.range(1,10).forEach(i->sj.add(i+""));
System.out.println(sj.toString());

// 构造参数,分隔符、前缀、后缀
StringJoiner sj3 = new StringJoiner(",", "p ", " s");
IntStream.range(20, 30).forEach(i->sj3.add(i+""));
System.out.println(sj3.toString());

StringJoiner sj4 = new StringJoiner(".","pp ", " ss");
IntStream.range(30,40).forEach(i->sj4.add(i+""));
// 合并,第一个sj3的前缀后缀为准
System.out.println(sj3.merge(sj4));

// 实战简单使用,结果1_2_3
String key = String.join("_", "1", "2", "3");
// 结果
1,2,3,4,5,6,7,8,9
p 20,21,22,23,24,25,26,27,28,29 s
p 20,21,22,23,24,25,26,27,28,29,30.31.32.33.34.35.36.37.38.39 s

其它功能:

  • setEmptyValue, 默认情况下的emptyValue是前缀加后缀, 用户可自定义emptyValue
  • merge(StringJoiner other),合并另外一个joiner
  • length, 当前长度,为空看emptyValue的长度

底层源码:

// 成员变量

// 前缀
private final String prefix;
// 分隔符
private final String delimiter;
// 后缀
private final String suffix;
// 字符串值,基于StringBuilder实现
private StringBuilder value;
// 空值,也就是未append字符串的初始值
private String emptyValue;

// 构造函数
public StringJoiner(CharSequence delimiter,
                        CharSequence prefix,
                        CharSequence suffix) {
        
    ....
    // make defensive copies of arguments
    this.prefix = prefix.toString();
    this.delimiter = delimiter.toString();
    this.suffix = suffix.toString();
    // 构造时就直接将emptyValue拼接好了,默认前缀和后缀
    this.emptyValue = this.prefix + this.suffix;
}

// 添加元素
public StringJoiner add(CharSequence newElement) {
    prepareBuilder().append(newElement);
    return this;
}

// append添加字符串底层代码,前缀和分隔符处理
private StringBuilder prepareBuilder() {
    // 从构造函数和类变量的声明可以看出,没有添加元素前stringbuilder是没有初始化的
    if (value != null) {   
        // 已经有元素存在的情况下,添加元素前先将分隔符添加进去  
        value.append(delimiter);
    } else {  
        // 没有元素存在的情况下先把前缀加进去    
        value = new StringBuilder().append(prefix);
    } 
    return value;
}
// toString方法
public String toString() {
    if (value == null) {
        // 这里如果没有自定义空值就是默认前缀+后缀
        return emptyValue;
    } else {
        // 为什么不直接value.toString()+suffix?????
        if (suffix.equals("")) {
            return value.toString();
        } else {
            int initialLength = value.length();
            // 返回完整字符串
            String result = value.append(suffix).toString();
            // value恢复到没有后缀时的字符串
            value.setLength(initialLength);
            return result;
        }
    }
}

// value去掉后缀,方便后面继续添加字符
public void setLength(int newLength) {   
    if (newLength < 0)
        throw new StringIndexOutOfBoundsException(newLength);  
    ensureCapacityInternal(newLength);       
    if (count < newLength) {  
        Arrays.fill(value, count, newLength, '\0');
    }
    count = newLength;
}
// 合并方法,前缀后缀保留调用merge一方
public StringJoiner merge(StringJoiner other) { 
    Objects.requireNonNull(other);  
    if (other.value != null) { 
        final int length = other.value.length(); 
        // 下面这段注释是说避免merge(this)时受影响,为什么?     
        // lock the length so that we can seize the data to be appended 
        // before initiate copying to avoid interference, especially when
        // merge 'this'       
        StringBuilder builder = prepareBuilder();  
        // stringBuilder的append方法,添加指定索引范围的字符串,也就是忽略other的前缀,起始prefix.length(),一直到尾部结束,后缀不在value上的
        builder.append(other.value, other.prefix.length(), length);  
    }
    return this;
}

参考:

https://mp.weixin.qq.com/s/yMD_bGyYgRHB7uEtFHkDuw

https://mp.weixin.qq.com/s/a6GPgllriwBUF9yjq7YwhA

你可能感兴趣的:(java,面试,java后端,java,字符串,源码)