Java中的StringStringBuffer和StringBuilder

8次阅读

共计 5776 个字符,预计需要花费 15 分钟才能阅读完成。

作为作为一个已经入了门的 java 程序猿,肯定对 Java 中的 String、StringBuffer 和 StringBuilder 都略有耳闻了,尤其是 String 肯定是经常用的。但肯定你有一点很好奇,为什么 java 中有三个关于字符串的类?一个不够吗!先回答这个问题,黑格尔曾经说过——存在必合理,单纯一个 String 确实是不够的,所以要引入 StringBuffer。再后来引入 StringBuilder 是另一个故事了,后面会详细讲到。
要了解为什么,我们就得先来看下这三者各自都有什么样的特点,有什么样的异同,对其知根知底之后,一切谜团都会被解开。

String

点开 String 的源码,可以发现 String 被定义为 final 类型,意味着它不能被继承,再仔细看其提供的方法,没有一个能对原始字符串做任何操作的,有几个开启了貌似是操作原字符串的,比如 replaceFirst replaceAll,点进去一看,其实是重新生成了一个新的字符串,对原始内容没有做任何修改。
是的,从实现的角度来看,它是不可变的,所有 String 的变更其实都会生成一个新的字符串,比 String str = "abcdefghijklmnopqrstuvwxy"; str = str + "z"; 之后新生成的 a - z 并不包含原来的 a -y,原来的 a - y 已经变成垃圾了。简单概括,只要是两个不同的字符,肯定都是两个完全不同不相关的对象,即便其中一个是从另一个 subString 出来的,两个也没有任何关系。如果是两个相同的字符串,情况比较复杂,可能是同一份也可能不是。如果在 JVM 中使用 G1gc,而且开启-XX:+UseStringDeduplication ,JVM 会对字符串的存储做优化,所以如果你的服务中有大量相同字符串,建议开启这个参数。
Java 作为一个非纯面向对象的语言,除了提供分装对象外,也提供了一些原始类型(比如:int long double char),String 的使用居然可以像用原始类型一样不需要 new,直接String str = "a" 这样声明,我觉得 String 更像是面向对象和非面向对象结合的一个产物。
String 最大的特点就是 __ 不可变__,这是它的优点,因为不可变意味着使用简单,没有线程安全的问题。但这也是它的缺点,因为每次变更都会生成一个新的字符串,明显太浪费空间了。

StringBuffer

我觉得 StringBuffer 是完全因为 String 的缺点而生的。我们日常使用 String 的过程中,肯定经常会用到字符串追加的情况,按 String 的实现,没次追加即便只是一个字符,都是生成一个完全不同的对象,如果这次操作很频繁很多的话会大幅提高内存的消耗,并且增加 gc 的压力。对于这种问题,StringBuffer 是如何解决的呢?我们直接从源码上来看。

但看 StringBuffer 里,几乎所有的方法都会调 super 父类,其实它所有的实现都是在 AbstractStringBuilder 里的。鉴于我们对其最长用的方法是 append,所以我们就从 append 入手,其实 append 也是 StringBuffer 比较核心的功能。

    /**
     * The value is used for character storage.
     */
    char[] value;
    
    AbstractStringBuilder(int capacity) {value = new char[capacity];
    }
    
    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;
    }
    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
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {newCapacity = minCapacity;}
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }
    

原来是 StringBuffer 父类 AbstractStringBuilder 有个 char 数组 value,用来存放字符串所有的字符,StringBuffer 默认初始大小是 16。StringBuffer 在每次 append 的时候,如果 value 的容量不够,就会申请一个容量比当前所需大一倍的字符数组,然后把旧的数据拷贝进去。这种一次性扩容一倍的方式,在我们之前 HashMap 源码浅析中已经看到过了。一次性多申请内存,虽然看起来会有大段的内存空闲,但其实可以减少 String append 时频繁创建新字符串的问题。
所以记住,如果你代码中对 String 频繁操作,千万不用用 String 而是选择用 StringBuffer 或者我们下面要讲的 StringBuilder。还有一个优化点,如果你能提前知道你字符串最大的长度,建议你在创建 StringBuffer 时指定其 capacity,避免在 append 时执行 ensureCapacityInternal,从而提升性能。
对于 StringBuffer 还有一个点没提到,注意看它源码的所有方法,除构造函数外,所有的方法都被__synchronized__修饰,意味着它是有个线程安全的类,所有操作查询方法都会被加同步,但是如果我们只是单线程呢,想用 StringBuffer 的优势,但又觉得加同步太多余,太影响性能。这个时候就轮到 StringBuilder 上场了。

StringBuilder


StringBuilder 从类图上看和 StringBuffer 完全没有任何区别,再打开它的源码,和 StringBuffer 一样几乎啥逻辑都没有,全是调调 super 父类 AbstractStringBuilder,它和 StringBuffer 最大的区别就是所有方法没有用 synchronized 修复,它不是一个线程安全的类,但也意味着它没有同步,在单线程情况下性能会优于 StringBuffer。

总结

看完上面内容,我觉得你应该知道上面时候用 String、什么时候用 StringBuffer、什么时候用 StringBuilder 了。

  1. 如果是常量字符串,用 String。
  2. 多线程环境下经常变动的字符串用 StringBuffer。
  3. 单线程经常变动的字符串用 StringBuilder。

彩蛋

我们来看个比较底层的东西,是关于 jvm 对 String 优化的,现在有如下代码。

public class StringTest {public static void main(String[] args) {
        String str = "abc";
        str = str + "d";
        str = str + "e";
    }
}

我们用 javac StringTest.java 编译成 class 文件,然后用 javap -c StringTest 生成字节码,内容如下

public class StringTest {public StringTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String abc
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String d
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_1
      23: return
}
➜  java git:(master) ✗ javap -c StringTest
Compiled from "StringTest.java"
public class StringTest {public StringTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String abc
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String d
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_1
      23: return
}
➜  java git:(master) ✗ javac StringTest.java
➜  java git:(master) ✗ javap -c StringTest  
Compiled from "StringTest.java"
public class StringTest {public StringTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String abc
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String d
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_1
      23: new           #3                  // class java/lang/StringBuilder
      26: dup
      27: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      30: aload_1
      31: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      34: ldc           #8                  // String e
      36: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      39: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      42: astore_1
      43: return
}

其实可以看出,java 底层实现字符串 + 的时候其实是用 StringBuilder 的 append()来实现的,如果有字符串的连续 +,jvm 用 StringBuilder append 也可以实现优化。

备注:源码来自 JDK11

本文由博客一文多发平台 OpenWrite 发布!

正文完
 0