String、StringBuffer 和 StringBuilder

1. String使用“+”拼接字符串

1.1 字符串常量内存分析。

首先我们要了解到,字符串常量是存储在常量池中的。在JDK1.7以前,常量池处于方法区当中。此时hotspot虚拟机对方法区的实现为永久代。对于永久代,大家需要了解他的一个特性,那就是GC不会对永久代内的数据进行垃圾回收

1.2 String类型特性。

String类型很重要的一个特性就是,字符串是不可变的,他们的值在创建后无法更改。看下面两行代码:

String s = "aaa";
s = "abc";

当执行第一行代码时,栈内存入栈了一个引用s,常量池存储了一个字符串常量“aaa”,s指向了“aaa”的地址。当执行第二行代码时,并不是"aaa"的内容改变成了“abc”,而是常量池中又存储了字符串常量“abc”,s指向的地址改为了“abc”的地址。“aaa”无引用指向,成为系统垃圾。

1.3 使用“+”拼接字符串时内存分析。

根据上述永久代和String类型的特性,当我们使用“+”拼接字符串时就会发生浪费空间的问题。详情请看下面一段代码:

String text1 = "abc";
String text2 = "def";
String text3 = "gh";
text1 = text1+text2+text3;
System.out.println(text1);

此段代码看上去没有任何问题,运行后也输出了“abcdefgh”。那究竟为何会浪费空间呢?但让我们来分析一下此时内存中的变化。
  首先,前三条代码执行后,栈内存中已经入栈了text1,text2,text3三个变量,常量池中也存储了三个字符串常量,栈中的三个变量分别指向了所对应的地址。如下图:
String、StringBuffer 和 StringBuilder_第1张图片

当执行第四行代码,字符串进行了拼接。此时注意,text1+text2+text3看起来是一个式子,但在执行过程中,内存中会新增加两个字符串常量。第一个是text1和text2拼接所生成的字符串(“abcdef”),第二个是第一个生成的字符串和text3字符串拼接后所生成的字符串(“abcdefgn”)。拼接完成后text1所指向的地址将会修改成最后生成的字符串地址,此时常量池中则出现了两个没有任何引用指向的常量,系统垃圾“abc”和“abcdef”。如下图:
String、StringBuffer 和 StringBuilder_第2张图片

而根据永久代的特性,这两个垃圾将不会被GC回收,成为“永久垃圾”!
此处看上去好像只是内存中多出了两个字符串而已,但如果业务规模大了起来,如果这种拼接是发生在一个大规模的循环体中,那么产生的垃圾可就不只是两个而已了,将会造成系统空间的大量浪费。

2. StringBuffer/StringBuilder拼接字符串分析

2.1 StringBuffer/StringBuilder对象创建时内存分析

通常我们创建StringBuffer/StringBuilder对象方式如下:(以StringBuffer为例)

StringBuffer s1 = new StringBuffer("abcd");

new关键字一出现,我们就应当知道,此时要创建一个对象了。而在Java中new出来的对象的实例存放在堆内存中,对象的引用存放在栈内存中。如下图:
String、StringBuffer 和 StringBuilder_第3张图片

2.2 StringBuffer/StringBuilder拼接字符串分析

与String类型不同的是,StringBuffer/StringBuilder是可变字符串,他们的值可以发生变化。请看以下代码:

StringBuffer s1 = new StringBuffer("abcd");
s1.append("efg");

第一行代码执行后内存已经分析过,如上图。当执行第二行代码时,append()方法的作用就是在末尾拼接字符串,由于StringBuffer是可变字符串,所以拼接即发生在当前StringBuffer的实例上,无需再新建实例。即改变了原实例内的字符串内容,s1依然指向原地址。内存变化如下:

以上例子均使用StringBuffer实现,StringBuilder进行拼接字符串操作时与StingBuffer相同。由此可见,使用StringBuffer/StringBuilder拼接字符串不会造成空间的浪费。
String、StringBuffer 和 StringBuilder_第4张图片

3.StringBuffer和StringBuilder的区别

3.1 字符修改上的区别

String、StringBuffer 和 StringBuilder_第5张图片

  1. 当对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类。
  2. StringBuffer 和 StringBuilder类的对象能够被多次的修改,并且不产生新的未使用对象。
  3. StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。
  4. 由于 StringBuilder 相较于 StringBuffer 有速度优势,多数情况下建议使用 StringBuilder类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。
    String、StringBuffer 和 StringBuilder_第6张图片

3.2 继承结构

String、StringBuffer 和 StringBuilder_第7张图片

3.3 源码详解

(1)线程安全

分析源码

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

StringBuffer:线程安全,StringBuilder:线程不安全

因为 StringBuffer 的所有公开方法都是 synchronized 修饰的,而 StringBuilder 并没有。

(2)缓冲区

StringBuffer 代码片段:

private transient char[] toStringCache;

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

StringBuilder 代码片段:

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

可以看出,StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。而 StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。所以,缓存冲这也是对 StringBuffer 的一个优化吧,不过 StringBuffer 的这个toString 方法仍然是同步的。

(3)性能

StringBuffer是线程安全的,它的所有公开方法都是同步的,StringBuilder 是没有对方法加锁同步的,所以毫无疑问,StringBuilder的性能要远大于StringBuffer。

(4) 使用场景
String、StringBuffer 和 StringBuilder_第8张图片

总结

StringBuffer 适用于用在多线程操作同一个 StringBuffer 的场景,如果是单线程场合 StringBuilder 更适合。

参考:https://blog.csdn.net/qq_41711633/article/details/115793928、https://blog.csdn.net/weixin_45393094/article/details/104526603

你可能感兴趣的:(java)