面试题系列之String,Stringbuffer,StringBuilder的区别(源码分析)

简介: 记得之前参加面试的时候被问到过String,Stringbuffer,StringBuilder的区别。我当时回答String是不可变的字符串,Stringbuffer,StringBuilder是可变的字符串,Stringbuffer是线程安全的,StringBuilder不是线程安全的,所以不能同步访问。心里想这下稳了,然后就没有然后了。现在想想这样回答别人能录用才怪。所以今天就从源码的角度剖析一下这三个字符串的区别到底在哪里。

前言


记得之前参加面试的时候被问到过String,Stringbuffer,StringBuilder的区别。我当时回答String是不可变的字符串,Stringbuffer,StringBuilder是可变的字符串,Stringbuffer是线程安全的,StringBuilder不是线程安全的,所以不能同步访问。心里想这下稳了,然后就没有然后了。现在想想这样回答别人能录用才怪。所以今天就从源码的角度剖析一下这三个字符串的区别到底在哪里。


1、String


先上源码,看看对于String类的定义:


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 */
    private int hash; // Default to 0
    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
     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类的源码我们可以看出,该类是被final关键字修饰的,并且String类实际是一个被final关键字修饰的char[]数组,所以实现细节上也是不允许改变,这就是String类的Immutable(不可变)属性(你知道String类为什么不能被继承吗?),导致每次对String的操作都会生成新的String对象导致每次对String的操作都会生成新的String对象,这样不仅效率低下,而且大量浪费有限的内存空间。


5b75361fe76d434fbe04421bee7ed497~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


上图是执行下方代码的一个过程:


String string ="abc";
string+="def";
复制代码


1、首先执行String string ="abc"在堆内存中开辟一块空间存储abc;


2、执行string+="def"的时候需要先在对内存中开辟一块空间存储def,然后再在堆内存中开辟一块空间存储最终的abcdef,然后将string的引用指向该堆内存空间。可以发现执行这样的短短两行代码需要在堆内存中开辟三次内存空间,造成了对内存空间资源的极大浪费。


但是在编程的过程中需要经常对字符串进行操作,所以java就引入了两个可以对此种变化字符串进行处理的类:StringBuffer类和StringBuild类。


2、StringBuffer和StringBuild


还是先上源码:


public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{}
复制代码


public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{}
复制代码


final修饰的char数组


public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
复制代码


stringbuilder和stringbuffer都继承了AbstractStringBuilder 但AbstractStringBuilder 中的char数组没有使用final修饰,这就是为什么string是不可变,但stringbuffer和stringbuilder是可变的


abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;
    /**
     * The count is the number of characters used.
     */
    int count;
    /**
     * This no-arg constructor is necessary for serialization of subclasses.
     */
复制代码


结合上述源码再看看 StringBuffer 和 StringBuilder 的类结构:


46c3086756c24877b340aadd42d85b2e~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


通过Stringbuffer和Stringbuilder的源码和类结构图,可以发现Stringbuilder和Stringbuffer都是继承了abstractStringbuilder这个抽象类,然后实现了Serializable, CharSequence接口。其次Stringbuilder和Stringbuffer的内部实现其实跟String是一样的,都是通过一个char类型的数组进行存储字符串的,但是是String类中的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer中的char数组没有被final修饰,是可变的。这就是StringBuilder和StringBuffer和String的区别。

那么为什么stringbuilder和stringbuffer一个是线程安全一个不是的呢?如果在多线程中分别使用stringbuilder和stringbuffer会是什么样呢?


首先来看看Stringbuffer的使用,多线程中使用stringbuffer:


public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        StringBuffer sb = new StringBuffer();
        for(int i=0;i<10;i++){
            //创建一个线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j=0;j<10000;j++){
                        sb.append("嗯");
                    }
                }
                //线程启动
            }).start();
        }
        //线程休眠300毫秒,这里要抛出异常
        Thread.sleep(300);
        //输出sb的长度是多少,理论上来说最后应该输出100000
        System.out.println(sb.length());
     }
}
复制代码


最后的输出结果是:


44c2150923f348259fc17c3b25b44316~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


与所想的理论值结果一样。接下来再看看使用StringBuilder的结果如何,在多线程中使用stringbuilder:


public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        StringBuilder sb = new StringBuilder();
        for(int i=0;i<10;i++){
            new Thread(new Runnable(){
                @Override
                public void run() {
                    for(int j=0;j<10000;j++){
                        sb.append("嗯");
                    }
                }
            }).start();
        }
        Thread.sleep(300);
        System.out.println(sb.length());
    }
}
复制代码


输出结果为:


07d380bb8b614b02939e995a0caf42fc~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


f8bb0bed27dc4899a2e908f8ea3bbcd6~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


123933471ab7405aa53ba1a3a384d583~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


理论上来说结果应该跟StringBuffer一样输出100000,但是实际结果是85560与预期结果不一样,而且多执行几次,每次结果也不一样(都小于预期值),而Stringbuffer执行多次结果都一样,而且有时候会抛ArrayIndexOutOfBoundsException异常(数组索引越界异常)。


所以我们可以发现在多线程中使用stringbuilder确实是线程不安全的。为什么实际的输出值不对呢?


前面提到过因为StringBuffer和StringBuilder都是继承了AbstractStringBuilder类,在AbstractStringBuilder中可以看到定义了一个char数组和一个count变量。


abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     *  翻译:存储字符的具体内容
     */
    char[] value;
    /**
     *
     * The count is the number of characters used.
     * 已经使用了的字符的数量    
     */
    int count;
复制代码


另外StringBuilder和StringBuffer通过append方法来进行字符串的增加,先看看Stringbuilder中的append方法调用的是AbstractStringBuilder的append的方法:


@Override
    public StringBuilder append(Object obj) {
        return append(String.valueOf(obj));
    }
    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
复制代码


在看看父类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=count+len,比如在上诉代码中,执行到count的值为99998的时候,新建一个len长度为1,但是当有两个线程同时执行到了count+=len的时候,他们的count的值都是99998,然后分别各自都执行了count+=len,则执行完之后的值都是99999,然后将值赋给count,则count最后的结果是99999,不是正确的100000,所以在多线程中执行stringbuilder的值始终会小于正确的结果。


但是StringBuilder和stringbuffer都是继承了abstractstringbuilder为什么结果不一样呢。既abstractstringbuilder中的append方法肯定都是一样的,再来看看stringbuffer中的append方法,append操作被synchronized 关键字修饰了:


@Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }
    @Override
   //append操作被synchronized 关键字修饰了
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
复制代码


可以发现stringbuffer中的append操作被synchronized关键字修饰了。这个关键字肯定不会陌生,主要用来保证多线程中的线程同步和保证数据的准确性。所以再多线程中使用stringbuffer是线程安全的。


在AbstractStringBuilder的append方法中有这样的两个个操作:


ensureCapacityInternal(count + len);   //1
str.getChars(0, len, value, count);    //2
复制代码


转到第一个操作方法的源码,可以发现这是一个是检查StringBuilder对象的原数组的大小是否能装下新的字符串的方法,如果装不下了就new一个新的数组,新的数组的容量是原来char数组的两倍,再通过CopyOf()方法将原数组的内容复制到新数组.


/**
     * For positive values of {@code minimumCapacity}, this method
     * behaves like {@code ensureCapacity}, however it is never
     * synchronized.
     * If {@code minimumCapacity} is non positive due to numeric
     * overflow, this method throws {@code OutOfMemoryError}.
     */
    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }
复制代码


然后第二步操作是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组里面。getchars源码如下:


public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
//1       
 if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
//2   
     if (srcEnd > value.length) {
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
//3   
     if (srcBegin > srcEnd) {
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }
复制代码


可以看到原来在这里会抛出StringIndexOutOfBoundsException的异常。


假设前面的代码中有两个线程a和线程b同时执行了append方法,并且都执行完了ensureCapacityInternal()方法,这个时候count的值为99997,如果当线程a执行完了,则轮到线程2继续执行,线程b执行完了append方法之后,count变成了99998,这个时候如果线程a执行到了上面的getchars方法的时候线程a得到的count的值就是99998了,而它本来的值应该是99997,所以在这个时候就会抛出ArrayIndexOutOfBoundsException的异常了。


3、总结:


String是final修饰符修饰的字符数组,所以是不可变的,如果操作的是少量的数据,则可以使用String;


StringBuilder和StringBuffer是可变的字符串数组;


StringBuilder是线程不安全的,因为Stringbuilder继承了父类abstractStringBuilder的append方法,该方法中有一个count+=len的操作不是原子操作,所以在多线程中采用StringBuilder会丢失数据的准确性并且会抛ArrayIndexOutOfBoundsException的异常。


StringBuffer是线程安全的因为他的append方法被synchronized关键字修饰了,所以它能够保证线程同步和数据的准确性。

因为StringBuffer是被synchronized修饰的,所以在单线程的情况下StringBuilder的执行效率是要比StringBuffer高的。所以一般在单线程下执行大量的数据使用StringBuilder,多线程的情况下则使用StringBuffer。

目录
相关文章
|
3天前
|
存储 Java
Java基础复习(DayThree):字符串基础与StringBuffer、StringBuilder源码研究
Java基础复习(DayThree):字符串基础与StringBuffer、StringBuilder源码研究
Java基础复习(DayThree):字符串基础与StringBuffer、StringBuilder源码研究
|
5天前
|
安全 Java 编译器
Java中String、StringBuilder和StringBuffer的区别
Java中String、StringBuilder和StringBuffer的区别
10 1
|
5天前
|
安全
String、StringBuuffer、StringBuilder三者的区别
String、StringBuuffer、StringBuilder三者的区别
|
5天前
|
缓存 安全 Java
【Java基础】String、StringBuffer和StringBuilder三种字符串对比
【Java基础】String、StringBuffer和StringBuilder三种字符串对比
9 0
|
4天前
|
Java 索引
String字符串常用函数以及示例 JAVA基础
String字符串常用函数以及示例 JAVA基础
|
5天前
|
Java 编译器 ice
【Java开发指南 | 第十五篇】Java Character 类、String 类
【Java开发指南 | 第十五篇】Java Character 类、String 类
24 1
|
5天前
|
Java API 索引
Java基础—笔记—String篇
本文介绍了Java中的`String`类、包的管理和API文档的使用。包用于分类管理Java程序,同包下类无需导包,不同包需导入。使用API时,可按类名搜索、查看包、介绍、构造器和方法。方法命名能暗示其功能,注意参数和返回值。`String`创建有两种方式:双引号创建(常量池,共享)和构造器`new`(每次新建对象)。此外,列举了`String`的常用方法,如`length()`、`charAt()`、`equals()`、`substring()`等。
16 0
|
3天前
|
存储 安全 Java
Java中的这些String特性可能需要了解下
Java中的String特性你知道哪些?虽然String很常见,通过源码可以看到String的值传递、字符串表和不可变性。本文基于JDK17说明。
9 1
|
5天前
|
Java
Java String 避免空指针的方法
Java String 避免空指针的方法
5 0
|
5天前
|
存储 缓存 安全
【 Java中String源码分析(JVM视角你不来看看?】
【 Java中String源码分析(JVM视角你不来看看?】
14 0