java基础之可变字符串StringBuilder和StringBuffer

本文详细比较了Java中的String、StringBuilder和StringBuffer三种字符串类的特点。包括它们的可变性、线程安全性、效率性和内存占用情况,并给出了不同场景下的使用建议。

StringBuilder和StringBuffer

阅读了上篇博客后,了解了String的一些特性,知道了String的不可变性,那么java中有没有提供可变的字符串呢?强大的java,当然有!他们就是StringBuilder和StringBuffer!下面我们就开始介绍他俩的一些特性和使用场景。

我们先来看一下,他们三哥们有什么区别?为啥可变,又为啥不可变?

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;

    // 此处省略一万行
}

细心的你们应该发现了,这边有一行注释叫  

/** The value is used for character storage. */  知道你们英文不好= =,帮你们谷歌翻译一下,大概就是(英文好的请忽略)

/**   该值用于字符存储。 */

嗯,这里就是说这个 value[]说存储字符串的值的,并且!它被final修饰了,final修饰变量还记得嘛,是说变量不可被改变,因此我们就知道了,奥,String的值不可变!

可变性源码解析

StringBuffer

我们也悄咪咪去看一下源码吧!

public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    /**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    static final long serialVersionUID = 3388685877147921107L;

    /**
     * Constructs a string buffer with no characters in it and an
     * initial capacity of 16 characters.
     */
    // 此处省略一万行
}

这里看到,好像没找着value?我翻到了最底下也没看到,但是这边注意到StringBuffer继承了一个类叫做AbstractStringBuilder,那是不是藏在父类里面呢?带着疑问我们去看一下

果然,在这边找到了,可以看到这里的value没有被final修饰,因此它是可变的!

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

同样悄咪咪的看一下源码

还是一样的,在StringBuilder类中,并没有找到value,有了之前的经验,那么就去父类中瞅一哈

 public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    /**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    static final long serialVersionUID = 3388685877147921107L;

    // 此处省略一万行
}

果然都是一个套路,成功的找到了没有被final修饰的value

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;
    
    // 此处省略一万行
}

线程安全

还是先解释一下什么是线程安全,大概意思就是说,同一时间只能有一个线程去访问我的内存资源(比如说一个方法,一个变量,或者一个代码块),这种就是叫做线程安全的,如果不能限制多个线程同时去访问内存资源,就会造成混乱,产生不必要的麻烦,这种我们就称为线程不安全

String

一般来说,String是没有线程安不安全的说法的,它都是不变的,那么不用想了,肯定安全啊!

对于可变的字符串类  StringBuilder 和 StringBuffer来说,才会有这方面的考虑,果然有取就有舍啊,增加了可变性的同时也带来了隐藏的危害。

StringBuffer

我们先来看源码,我这边随便copy了几个源码中的方法,来看看

    @Override
    public synchronized int codePointBefore(int index) {
        return super.codePointBefore(index);
    }

    /**
     * @since     1.5
     */
    @Override
    public synchronized int codePointCount(int beginIndex, int endIndex) {
        return super.codePointCount(beginIndex, endIndex);
    }

    /**
     * @since     1.5
     */
    @Override
    public synchronized int offsetByCodePoints(int index, int codePointOffset) {
        return super.offsetByCodePoints(index, codePointOffset);
    }

    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    @Override
    public synchronized void getChars(int srcBegin, int srcEnd, char[] dst,
                                      int dstBegin)
    {
        super.getChars(srcBegin, srcEnd, dst, dstBegin);
    }

    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     * @see        #length()
     */
    @Override
    public synchronized void setCharAt(int index, char ch) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        toStringCache = null;
        value[index] = ch;

有没有注意到什么?很好,这边的方法全都被synchronized关键字修饰了,之前我们讲修饰符的时候提到过这个,是被synchronized修饰的方法叫做同步方法,也就是线程安全的。那么之前也说到了有取就有舍,提高了安全性的同时,又降低了执行的效率,果然想两全其美真的难啊!所以再多线程的场景下,我们还是尽量去使用StringBuffer,不能为了一点点效率而提升了安全隐患,得不偿失。

StringBuilder

一言不合看源码,再随便copy几个方法看看

@Override
    public StringBuilder append(double d) {
        super.append(d);
        return this;
    }

    /**
     * @since 1.5
     */
    @Override
    public StringBuilder appendCodePoint(int codePoint) {
        super.appendCodePoint(codePoint);
        return this;
    }

    /**
     * @throws StringIndexOutOfBoundsException {@inheritDoc}
     */
    @Override
    public StringBuilder delete(int start, int end) {
        super.delete(start, end);
        return this;
    }

    /**
     * @throws StringIndexOutOfBoundsException {@inheritDoc}
     */
    @Override
    public StringBuilder deleteCharAt(int index) {
        super.deleteCharAt(index);
        return this;
    }

    /**
     * @throws StringIndexOutOfBoundsException {@inheritDoc}
     */
    @Override
    public StringBuilder replace(int start, int end, String str) {
        super.replace(start, end, str);
        return this;
    }

    /**
     * @throws StringIndexOutOfBoundsException {@inheritDoc}
     */
    @Override
    public StringBuilder insert(int index, char[] str, int offset,
                                int len)
    {
        super.insert(index, str, offset, len);
        return this;
    }

这边可以看到,StringBuilder类中的方法,是没有synchronized修饰的,也就是所谓的线程不安全,但是好处是,它的效率高于StringBuffer,所以单线程的时候,推荐使用StringBuilder,速度快。

效率性

String

因为String是不可变对象,因此每次进行字符串拼接的时候,都是重新进行了对象的创建,十分影响效率。

public static void main(String[] args) {

        String str = "";
        long beginTime = System.currentTimeMillis();

        for (int i = 0; i < 10000; i++){
            str = str + i;
        }

        long endTime = System.currentTimeMillis();

        System.out.println("执行时间:" + (endTime - beginTime));  // 执行时间:403
    }

这里的执行之间为403毫秒

StringBuilder

相对比String,肯定是要快上不少的,因为不用每次都是创建新对象,只是想“容器” value 中动态的添加数据。

public static void main(String[] args) {

        StringBuilder str = new StringBuilder();
        long beginTime = System.currentTimeMillis();

        for (int i = 0; i < 10000; i++){
            str.append(i);
        }

        long endTime = System.currentTimeMillis();

        System.out.println("执行时间:" + (endTime - beginTime));  // 执行时间:3
    }

哪尼?只要3毫秒,好像是快很多奥

StringBuffer

之前提到了,StringBuffer是线程安全的,那么它的速度理论上来说应该会比StringBuilder要慢一点,看看结果

    public static void main(String[] args) {

        StringBuffer str = new StringBuffer();
        long beginTime = System.currentTimeMillis();

        for (int i = 0; i < 10000; i++){
            str.append(i);
        }

        long endTime = System.currentTimeMillis();

        System.out.println("执行时间:" + (endTime - beginTime));  // 执行时间:3
    }

好像区别不大?几乎速度是一模一样的!

那我们把数字调大一点可能会有所差别,来看看

public static void main(String[] args) {

        StringBuilder str = new StringBuilder();
        long beginTime = System.currentTimeMillis();

        for (int i = 0; i < 9999999; i++){
            str.append(i);
        }

        long endTime = System.currentTimeMillis();

        System.out.println("执行时间:" + (endTime - beginTime));  // 执行时间:409
    }
public static void main(String[] args) {

        StringBuffer str = new StringBuffer();
        long beginTime = System.currentTimeMillis();

        for (int i = 0; i < 9999999; i++){
            str.append(i);
        }

        long endTime = System.currentTimeMillis();

        System.out.println("执行时间:" + (endTime - beginTime));  // 执行时间:503
    }

观察结果可以看到,好像还是StringBuilder略胜一筹,说明他俩在数量级较小的情况下,效率几乎一致!

内存占比

/**
*  定义三个方法做测试
*/

private static void StringTest() {

        String s = "";
        for (int i = 0; i < 10000; i++) {
            s += "qwertyuiopasdfghjklzxcvbnmqazwsxedcrfvtgbyhnujmiklopplokmnjiuhbvgytfcxdrzsewaq";
        }
    }

    private static void StringBufferTest() {

        StringBuffer stringBuffer = new StringBuffer("");
        for (int i = 0; i < 10000; i++) {
            stringBuffer = stringBuffer
                    .append("qwertyuiopasdfghjklzxcvbnmqazwsxedcrfvtgbyhnujmiklopplokmnjiuhbvgytfcxdrzsewaq");
        }
    }

    private static void StringBuilderTest() {

        StringBuilder stringBuilder = new StringBuilder("");
        for (int i = 0; i < 10000; i++) {
            stringBuilder = stringBuilder
                    .append("qwertyuiopasdfghjklzxcvbnmqazwsxedcrfvtgbyhnujmiklopplokmnjiuhbvgytfcxdrzsewaq");
        }
    }

来测试一下

StringTest

public static void main(String[] args) {
        StringTest();
        long memory = Runtime.getRuntime().totalMemory()
                - Runtime.getRuntime().freeMemory();
        System.out.println("memory: " + memory);  // memory: 63437192
    }

StringBuilderTest

public static void main(String[] args) {
        StringBuilderTest();
        long memory = Runtime.getRuntime().totalMemory()
                - Runtime.getRuntime().freeMemory();
        System.out.println("memory: " + memory);  // memory: 9187648
    }

StringBufferTest

public static void main(String[] args) {
        StringBufferTest();
        long memory = Runtime.getRuntime().totalMemory()
                - Runtime.getRuntime().freeMemory();
        System.out.println("memory: " + memory);  // memory: 9187648
    }

可以看到,String最小,而StringBuilder居然和StringBuffer是一样的,想想也是,它们的数据都存放在value里,当然一样了!

好了,看到这里,我们也该总结一下了,它们的区别在哪里

  • 可变性:String不可变,StringBuilder和StringBuffer可变
  • 线程安全性:String和StringBuffer线程安全,StringBuilder线程不安全
  • 效率性:StringBuilder>StringBuffer>String
  • 内存占比:String>StringBuilder=StringBuffer
  • 使用场景:不经常做拼接等操作的,使用量较小的的用String;经常改变字符串本身,并且又是多线程操作的,在乎执行效率的,用StringBuffer;经常改变字符串本身,单线程操作,在乎执行效率的,用StringBuilder。

具体的一些它们里面的一些方法,留给大家自己去查阅资料,做练习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值