深入了解Java中的StringBuilder与StringBuffer
- 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 AbstractStringBuilderimplements java.io.Serializable, CharSequence
{// ...public StringBuilder append(String str) {super.append(str);return this;}public String toString() {// Create a copy, don't share the arrayreturn new String(value, 0, count);}// ...其他方法
}
但是因为方法上没有synchronized关键字,所以StringBuilder多线程情况不安全 ,如果要在多线程环境下修改字符串,你到时候可以使用 ThreadLocal 来避免多线程冲突
public class ThreadSafeStringBuilder {// 使用ThreadLocal为每个线程提供独立的StringBuilder对象private static final ThreadLocal<StringBuilder> 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
- 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/StringBuilder3: dup4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V7: new #4 // class java/lang/String10: dup11: ldc #5 // String spring13: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;19: new #4 // class java/lang/String22: dup23: ldc #8 // String 葵花宝典25: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V28: 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_135: return
可以发现Java 编译器将字符串拼接操作(+)转换为了 StringBuilder 对象的 append 方法,然后再调用 StringBuilder 对象的 toString 方法返回拼接后的字符串
- 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 codeint oldCapacity = value.length >> coder;if (minimumCapacity - oldCapacity > 0) {value = Arrays.copyOf(value,newCapacity(minimumCapacity) << coder);}
}private int newCapacity(int minCapacity) {// overflow-conscious codeint oldCapacity = value.length >> coder;int newCapacity = (oldCapacity << 1) + 2;if (newCapacity - minCapacity < 0) {newCapacity = minCapacity;}int SAFE_BOUND = MAX_ARRAY_SIZE >> coder;return (newCapacity <= 0 || SAFE_BOUND - newCapacity < 0)? hugeCapacity(minCapacity): newCapacity;
}
ensureCapacityInternal(int minimumCapacity) 方法用于确保当前字符序列的容量至少等于指定的最小容量 minimumCapacity。如果当前容量小于指定的容量,就会为字符序列分配一个新的内部数组。新容量的计算方式如下:
如果指定的最小容量大于当前容量,则新容量为两倍的旧容量加上 2。为什么要加 2 呢?这是因为在某些情况下,仅仅将容量加倍可能仍然不足以容纳更多的字符。例如,对于非常小的字符串(比如空的或只有一个字符的 StringBuilder),仅仅将容量加倍可能仍然不足以容纳更多的字符。因此,加上 2 提供了一个最小的增长量,确保即使对于很小的初始容量,扩容后也能至少添加一些字符而不需要立即再次扩容。
如果指定的最小容量小于等于当前容量,则不会进行扩容,直接返回当前对象。这样做是为了避免不必要的内存浪费和性能开销。
3.4 StringBuilder的 reverse 方法
public StringBuilder reverse() {super.reverse();return this;
}
StringBuilder类的reverse() 方法实际调用AbstractStringBuilder类中reverse()方法。该方法会检查当前字符序列中的字符是否够用,如果不够用则会进行扩容,并将指定字符串追加到字符序列的末尾
public AbstractStringBuilder reverse() {byte[] val = this.value;int count = this.count;int coder = this.coder;int n = count - 1; // 字符序列的最后一个字符的索引if (COMPACT_STRINGS && coder == LATIN1) {for (int j = (n-1) >> 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,并交换这两个索引位置的字符。