面试题系列之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。

目录
相关文章
|
4天前
|
安全
String、StringBuffer、StringBuilder的区别
String 由 char[] 数组构成,使用了 final 修饰,对 String 进行改变时每次都会新生成一个 String 对象,然后把指针指向新的引用对象。 StringBuffer可变并且线程安全;有一定缓冲区容量,字符串大小没超过容量,不会重新分配新的容量,适合多线程操作字符串; StringBuiler可变并且线程不安全。速度比StringBuffer更快,适合单线程操作字符串。 操作少量字符数据用 String;单线程操作大量数据用 StringBuilder;多线程操作大量数据用 StringBuffer
|
19天前
|
存储 缓存 网络协议
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点,GET、POST的区别,Cookie与Session
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点、状态码、报文格式,GET、POST的区别,DNS的解析过程、数字证书、Cookie与Session,对称加密和非对称加密
|
1月前
|
编译器
经典面试题:变量的声明和定义有什么区别
在编程领域,变量的“声明”与“定义”是经典面试题之一。声明告诉编译器一个变量的存在,但不分配内存,通常包含变量类型和名称;而定义则为变量分配内存空间,一个变量必须至少被定义一次。简而言之,声明是告知变量形式,定义则是实际创建变量并准备使用。
|
26天前
|
存储 安全 Java
String、StringBuffer 和 StringBuilder 的区别
【10月更文挑战第21天】String、StringBuffer 和 StringBuilder 都有各自的特点和适用场景。了解它们之间的区别,可以帮助我们在编程中更合理地选择和使用这些类,从而提高程序的性能和质量。还可以结合具体的代码示例和实际应用场景,进一步深入分析它们的性能差异和使用技巧,使对它们的理解更加全面和深入。
15 0
|
1月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
123 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
1月前
|
前端开发 小程序 JavaScript
面试官:px、em、rem、vw、rpx 之间有什么区别?
面试官:px、em、rem、vw、rpx 之间有什么区别?
46 0
|
2月前
|
Java 索引
java基础(13)String类
本文介绍了Java中String类的多种操作方法,包括字符串拼接、获取长度、去除空格、替换、截取、分割、比较和查找字符等。
39 0
java基础(13)String类
|
1月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
56 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
|
1月前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
26 2
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1

热门文章

最新文章

下一篇
无影云桌面