stringbuilder去掉最后一个字符_jdk 源码系列之StringBuilder、StringBuffer

5d96fdfd825a559d9471a613c85a1d1c.png

jdk 源码系列之StringBuilder、StringBuffer


前言

StringBuilderStringBuffer 经常使用到,分析 StringBuilderStringBuffer 源码、通过对比加深对这两个类的了解,以及以后更好的使用。

父类

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


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

从上面的继承,我们可以知道,无论是 StringBuilder 亦或是 StringBuffer 都是继承相同的父类 AbstractStringBuilder ,这说明 StringBuilderStringBuffer 他们都是 AbstractStringBuilder 子类,而且操作继承父类的功能相同,差异在于子类实现的不同。我们先来看看 AbstractStringBuilder 的具体实现。

AbstractStringBuilder

先来看看构造方法。

/**
 * This no-arg constructor is necessary for serialization of subclasses.
 */
AbstractStringBuilder() {
}

/**
 * This no-arg constructor is necessary for serialization of subclasses.
 */
AbstractStringBuilder(int capacity) {
    value = new char[capacity];
}

提供两个构造函数,一个是无参构造、一个是有参构造。其中,有参构造,初始化了 char[] 的大小。

了解到这里够了,我们来看看 StringBuilder 以及 StringBuffer 这些子类,具体调了 AbstractStringBuilder 哪些方法。

StringBuilder

提供三种 new 对象

StringBuilder stringBuilder = new StringBuilder();

StringBuilder stringBuilder = new StringBuilder("sin sy");

StringBuilder stringBuilder = new StringBuilder(10);

点进去

public StringBuilder() {
    super(16);
}

public StringBuilder(int capacity) {
    super(capacity);
}

public StringBuilder(String str) {
    super(str.length() + 16);
    append(str);
}

前面的三种构造、一次调用如上图所示。 调用用了父类的 AbstractStringBuilder 构造构造方法,初始化 char[] 的大小。看来是默认 char[] 是 16。也就是说 StringBuilder 默认大小是16。以及提供初始化 char[] 大小。如果传入的 String 类型,则 char[] 的大小是字符串的长度 + 16。

这里和 HashMap 差不多,都有初始化大小。可能有几个问题,比如使用 StringBuilder 的时候,拼接字符串的长度很小,远远没到 16 的长度。导致空置了许多 char 空间,而这些没用被引用的空间,会触发 GC 回收,进而可能触发 Full GC 影响整个程序性能,所以如果能知道具体长度,尽可能的指定初始化值,优化性能。

接下来点开看 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;
}

进来的时候,首先判断是不是 null,如果是的话,直接写上 null,标记当前 char 的位置。

private AbstractStringBuilder appendNull() {
    int c = count;
    ensureCapacityInternal(c + 4);
    final char[] value = this.value;
    value[c++] = 'n';
    value[c++] = 'u';
    value[c++] = 'l';
    value[c++] = 'l';
    count = c;
    return this;
}

使用了类的成员变量 count

/**
 * The value is used for character storage.
 */
char[] value;

/**
 * The count is the number of characters used.
 */
int count;

其中 value 就是字符存储空间,初始化的使用用到。

count 是记录字符数组的位置。我看了一会,没有找到初始化这个的值,那么这里的 count = 0。

继续, 用到了 ensureCapacityInternal ,看看里面有啥,不过看英文名叫做 确保内部容量。应该是确认容量大小。

private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    // 判断扩容
    if (minimumCapacity - value.length > 0) {
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity));
    }
}

private int newCapacity(int minCapacity) {
    // overflow-conscious code
    // 扩容处理,扩当前容器的 2倍 + 2 容
    int newCapacity = (value.length << 1) + 2;

    // 判断当前的字符串大小,是否超过扩容之后的大小
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }

    // 超过最大数量,则直接直接抛出 OOM,反之则直接使用扩容后的大小
    return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
        ? hugeCapacity(minCapacity)
        : newCapacity;
}


private int hugeCapacity(int minCapacity) {
    if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
        throw new OutOfMemoryError();
    }
    return (minCapacity > MAX_ARRAY_SIZE)
        ? minCapacity : MAX_ARRAY_SIZE;
}

当处理的字符串长度,大于当前所能容下的大小,则以 当前容量 * 2 + 2 大小扩容,反之则不能处理。同时进行数组间的复制。这也是什么 StringBuilder 能动态拼接字符串,而不是固定大小就不能再申请的原因。

当超过所能容纳的最大字符时,2 31 - 9 (MAX_VALUE = 2 31 -1,另外它自己 - 8) 的大小,则直接 OOM。

由于这里存在动态扩容,char[] 的空间也可能存在空置,未被使用,引起 GC 回收。同时这里还有数组间的复制,导致性能有所下降,所以还是能确定大小,则直接初始化大小。

继续 append 方法。

appendNull 和 append 差不过,每次都在判断是否需要扩容,然后记录 char[] 的位置。

最后,一般我们都要将 StringBuilder toString()。

public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

输出的时候,直接使用 char[] 数组,从 0 到 count 所记录的位置,产生一个对象 String 返回。

另外。

StringBuilder 还有提供了许多有趣的东西。 字符串的反转 StringBuilder 类拼接 插入某个位置 以及返回当前数组位置 等等

StringBuilder 的总结

  1. 成员变量 count 记录数组位置,使用的是 int 类型,在多线程中未涉及到原子性,可能导致 count 的数值有误,从而导致最后输出的字符串有问题。
  2. 初始化的时候最好指定大小,避免触发 GC、以及数组之间的复制操作。

StringBuffer

也是提供三个构造方法。和 StringBuilder 没去。都是默认16,也可以自行初始化、或者直接写字符串。

不过在还新增了一个成员变量 toStringCache

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


@Override
public synchronized String toString() {
    if (toStringCache == null) {
        toStringCache = Arrays.copyOfRange(value, 0, count);
    }
    return new String(toStringCache, true);
}

根据 transient 的特性,我们可以知道这个 toStringCache 不会持久化,作为一个缓冲的存在。感觉这样做的原因是,这个值一直在改变,只有等到最后输出的时候才用到。可能节省空间吧。

另外除了构造方法,所有的方法都加上了 synchronized 修饰,来保证线程安全。所有的方法和 StringBuilder 差不多。

StringBuffer 总结

  1. 线程安全。
  2. 由于所有的方法都加上了 synchronized 所以效率是远满于 StringBuilder 的拼接速度。

总结

StringBuffer vs StringBuilder

线程安全速度操作业务场景
StringBuffer安全多线程
StringBuilder不安全很快单线程

优化建议

  1. 尽可能的指定初始化大小,避免频繁的扩容、以及数组之间的复制,到而导致空置数组空间,触发 GC。
  2. 确定业务模型之后,是单线程或者业务并发不高,可以选择 StringBuilder,来拼接字符串。
  3. 高并发底下,请选择 StringBuffer 来拼接字符串。性能差可以接受,但是出问题,是不能容忍的。

声明

作者: Sinsy
本文链接:https://blog.sincehub.cn/2020/09/29/jdk-StringBuilder-StringBuffer/
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文声明。
如您有任何商业合作或者授权方面的协商,请给我留言:550569627@qq.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: StringBuilderStringBuffer都是Java中的字符串缓冲区类,它们的作用是在字符串操作时提高效率。它们的主要区别在于线程安全性和性能。StringBuffer是线程安全的,但是性能相对较差;而StringBuilder则是非线程安全的,但是性能更好。在单线程环境下,建议使用StringBuilder,而在多线程环境下,建议使用StringBuffer。 ### 回答2: StringBuilderStringBuffer都是Java中用于操作字符串的类,它们之间的主要区别在于线程安全性和性能上的差异。 StringBuffer一个线程安全的可变字符串类,它的方法都被synchronized关键字修饰,因此多个线程可以同时访问同一个StringBuffer对象,确保操作的原子性和一致性。这种线程安全性会带来额外的开销,在多线程环境下使用较为合适。 而StringBuilder一个非线程安全的可变字符串类,它的方法没有被synchronized修饰,因此在单线程环境下性能比StringBuffer更好。由于不需要考虑线程安全性,StringBuilder的方法在执行速度上比StringBuffer更快,并且不会导致额外的开销。 除了线程安全性和性能上的差异,StringBuilderStringBuffer的使用方法几乎相同。它们都提供了一系列用于操作字符串的方法,例如追加字符串、插入字符串、删除字符串等。可以通过调用它们的构造函数来创建一个空的StringBuilderStringBuffer对象,并使用append()方法来进行字符串的操作。最后,通过调用toString()方法可以将结果转换为String类型。 综上所述,如果需要在多线程环境中操作字符串或者考虑线程安全问题,应该使用StringBuffer;如果在单线程环境中操作字符串或者追求更高的性能,应该使用StringBuilder。 ### 回答3: StringBuilderStringBuffer是Java中用于处理字符串的两个类,它们有很多相似之处,但也有一些不同点。 首先,它们的相似之处在于: 1. 都是可变的字符串类,可以通过添加、插入、删除和修改字符来操作字符串。 2. 都可以通过toString()方法获得最终的字符串结果。 3. 都是线程安全的,可以在多线程环境下使用,但StringBuffer的方法是同步的,而StringBuilder的方法是非同步的。 然而,它们的不同之处在于: 1. 效率:StringBuilderStringBuffer的效率更高,因为StringBuilder的方法是非同步的,不需要考虑线程安全问题,而StringBuffer的方法是同步的,需要进行线程安全的操作。 2. 可变性:StringBuilderJDK 1.5引入的,它是非线程安全的,因此在单线程环境下性能更好。而StringBufferJDK 1.0引入的,它是线程安全的,因此在多线程环境中性能较好。 3. 内部实现:StringBuilderStringBuffer都使用可变的字符数组来存储字符串,但是StringBuffer在对字符串进行修改时会创建一个新的字符数组,而StringBuilder则对原有的数组进行修改,减了内存开销。 总的来说,如果在单线程环境下进行字符串操作,建议使用StringBuilder,因为它效率更高。而如果在多线程环境下进行字符串操作,建议使用StringBuffer,因为它是线程安全的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值