深入了解Java中的StringBuilder与StringBuffer

2024年 3月 7日 44.5k 0

1. StringBuffer和StringBuilder的区别

因为字符串不可变,当字符串拼接(尤其是使用+号操作符)时,需要考量性能的问题,不多毫无顾忌的创建太多String对象,从而对内存造成不必要压力。

因此Java专门设计StringBuilder类来解决该问题

public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequence {

    public StringBuffer() {
        super(16);
    }
    
    public synchronized StringBuffer append(String str) {
        super.append(str);
        return this;
    }

    public synchronized String toString() {
        return new String(value, 0, count);
    }

    //...方法
}

从上面代码我们可以发现StringBuffer在进行字符串操作时,方法都添加上synchronized关键字进行同步,这主要是考虑到多线程环境下安全问题。因为加了synchronized,所以在非多线程下,执行效率就会比较低,这是添加了没必要的锁。

考虑到性能问题,Java又给StringBuffer添加了一个孪生兄弟StringBuilder在方法上没有添加synchronized关键字,因此无论单线程还是多线程效率都会高。

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

    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

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

    // ...其他方法
}

但是因为方法上没有synchronized关键字,所以StringBuilder多线程情况不安全 ,如果要在多线程环境下修改字符串,你到时候可以使用 ThreadLocal 来避免多线程冲突。

public class ThreadSafeStringBuilder {
    // 使用ThreadLocal为每个线程提供独立的StringBuilder对象
    private static final ThreadLocal threadLocalStringBuilder = ThreadLocal.withInitial(StringBuilder::new);

    public static void appendString(String str) {
        // 获取当前线程的StringBuilder对象
        StringBuilder stringBuilder = threadLocalStringBuilder.get();
        // 在StringBuilder对象上执行字符串拼接操作
        stringBuilder.append(str);
    }

    public static String getString() {
        // 获取当前线程的StringBuilder对象
        StringBuilder stringBuilder = threadLocalStringBuilder.get();
        // 返回StringBuilder对象的字符串表示
        return stringBuilder.toString();
    }

    public static void main(String[] args) {
        // 创建多个线程并发执行字符串拼接操作
        Runnable task = () -> {
            for (int i = 0; i < 10; i++) {
                appendString(Thread.currentThread().getName() + "-" + i + " ");
            }
            // 输出当前线程的字符串结果
            System.out.println(Thread.currentThread().getName() + ": " + getString());
            // 清空当前线程的StringBuilder对象,以便下次使用
            threadLocalStringBuilder.get().setLength(0);
        };

        // 启动多个线程
        Thread[] threads = new Thread[5];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(task);
            threads[i].start();
        }

        // 等待所有线程执行完成
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

注意:实际开发中,StringBuilder 的使用频率也是远高于 StringBuffer,甚至可以说,StringBuilder 完全取代了 StringBuffer。

2. StringBuilder使用

在深入解析 String.intern() 说过编译器遇到 + 号这个操作符的时候,会将 new String("spring") + new String("葵花宝典") 编译代码如下:

new StringBuilder().append("spring").append("葵花宝典").toString();

虽然过程我们看不见,这正是 Java 的只能之处,Java可以在编译的时帮我们做很多优化,这样既可以提高我们的开发效率(+ 号写起来比创建 StringBuilder 对象便捷得多),也不会影响 JVM 的执行效率。

如果我们使用 javap 反编译 new String("spring") + new String("葵花宝典") 的字节码的时候,也是能看出 StringBuilder 的影子。

0: new           #2                  // class java/lang/StringBuilder
       3: dup
       4: invokespecial #3                  // Method java/lang/StringBuilder."":()V
       7: new           #4                  // class java/lang/String
      10: dup
      11: ldc           #5                  // String spring
      13: invokespecial #6                  // Method java/lang/String."":(Ljava/lang/String;)V
      16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: new           #4                  // class java/lang/String
      22: dup
      23: ldc           #8                  // String 葵花宝典
      25: invokespecial #6                  // Method java/lang/String."":(Ljava/lang/String;)V
      28: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      31: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      34: astore_1
      35: return

可以发现Java 编译器将字符串拼接操作(+)转换为了 StringBuilder 对象的 append 方法,然后再调用 StringBuilder 对象的 toString 方法返回拼接后的字符串。

3. StringBuilder的内部实现

3.1.  StringBuilder的toString()方法

public String toString() {
    return new String(value, 0, count);
}

value 是一个 char 类型的数组

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

StringBuilder创建对象是,会给value分配内存空间(初始容量16),来存储字符串。

public StringBuilder() {
    super(16);
}

随着字符串不断拼接,value数组长度会自动进行扩容操作,将字符数组长度增加到足够容纳新字符串的大小。value动态扩容的过程类似于ArrayList中的扩容机制,确保了在拼接大量字符串时的高效性

3.2.  StringBuilder的append(String str) 方法

public StringBuilder append(String str) {
    super.append(str);
    return this;
}

StringBuilder类的append(String str) 方法实际调用AbstractStringBuilder类中append方法。该方法会检查当前字符序列中的字符是否够用,如果不够用则会进行扩容,并将指定字符串追加到字符序列的末尾。

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

AbstractStringBuilder类的append(String str) 方法将指定的字符串追加到当前字符序列中。如果指定字符串为 null,则追加字符串 "null";否则,该方法会检查指定字符串的长度,根据当前字符序列中已有字符的数量以及指定字符串的长度来判断是否需要扩容。如果需要扩容,则会分配一个新的字符数组,将原有字符序列的内容复制到新的字符数组中,并将指定字符串的内容追加到新字符数组的末尾。这样就确保了在追加字符串时,字符序列的容量始终能够满足当前字符数量的需求,避免了不必要的内存浪费说明:扩容调用方法ensureCapacityInternal(int minimumCapacity)方法,扩容之后,将指定字符串的字符拷贝到字符序列中。

3.3. AbstractStringBuilder的ensureCapacityInternal(int minimumCapacity)方法

private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    int oldCapacity = value.length >> coder;
    if (minimumCapacity - oldCapacity > 0) {
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity) > coder;
    int newCapacity = (oldCapacity > coder;
    return (newCapacity > 1; j >= 0; j--) {
                int k = n - j; // 计算相对于 j 对称的字符的索引
                byte cj = val[j];  // 获取当前位置的字符
                val[j] = val[k]; // 交换字符
                val[k] = cj; // 交换字符
            }
        } else {
            StringUTF16.reverse(val, count);
        }
        return this;  // 返回反转后的字符串构建器对象
    }

1.初始化:

n表示字符串中最后一个字符索引

2.字符串反转:

  • 方法通过一个 for 循环遍历字符串的前半部分和后半部分,这是一个非常巧妙的点,比从头到尾遍历省了一半的时间。(n-1) >> 1 是 (n-1) / 2 的位运算表示,也就是字符串的前半部分的最后一个字符的索引。
  • 在每次迭代中,计算出与当前索引 j 对称的索引 k,并交换这两个索引位置的字符。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论