String, StringBuffer 和 StringBuilder之间的区别

String, StringBuffer 和 StringBuilder

  1. 可变性

    • String不可变

    • StringBuffer 和 StringBuilder 可变

  2. 线程安全

    • String 不可变,因此是线程安全的

    • StringBuilder不是线程安全的

    • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

StringBuffer的append方法

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

拼接字符串建议StringBuilder

源码

String源码,存放字符串的地方

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    
    ...
}

StringBuilder自身没有自定义存储的容器,而是继承了其父类的容器

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**The value is used for character storage.*/
    char[] value;
    
    ...
}

不一样的地方就在于 String 的value是不可变的,而StringBuilder 的value是可变的

String拼接字符串案例

String s1 = "第1个字符串";
String s2 = "第2个字符串";
String str = s1 + s2;

以上操作可以看成是

//这里只作为理解,相当于新开拓一个字符数组,然后复制
final char c1[] = {'第','1','个','字','符','串'};
final char c2[] = {'第','2','个','字','符','串'};
final char c3[] = new char[12];
c3[] =  {'第','1','个','字','符','串','第','2','个','字','符','串'};
  • 创建s1的时候其实就是创建了第一个不可变的char[]数组,创建s2的时候创建了第二个不可变的char[]数组

  • 创建str的时候其实就是另外又创建了一个数组,再将s1和s2的数据复制到str中

StringBuilder拼接字符串案例

StringBuilder sb = new StringBuilder();
System.out.println("初始容量:" + sb.capacity());
sb.append("十五个十五个十五个十五个十五个");
System.out.println("追加15个字后sb容量:" + sb.capacity());
sb.append("一");
System.out.println("已经十六个字SB容量:" + sb.capacity());
sb.append("添加");
System.out.println("超过16个字的SB容量:" + sb.capacity());

输出

初始容量:16
追加15个字后sb容量:16
已经十六个字SB容量:16
超过16个字的SB容量:34

StringBuilder特征:StringBuilder初始化容量是16(无参构造)

public StringBuilder() {
  super(16);
}
  • 追加之前会计算一次容量,大于所需容量则会重新创建一个char[]数组,计算规则是 newCapacity = (value.length << 1) + 2; 也就是原来长度*2 + 2

  • StringBuilder在运算的时候每次会计算容量是否足够,如果所需容量不小于自身容量,那么就会重新分配一个自身容量两倍 +2 的char[].

//追加操作
public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    //count是当前char[]数组的使用大小,len是要追加的字符串的长度
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

//这个方法是确保 char[]数组的大小能装下新追加的字符串
private void ensureCapacityInternal(int minimumCapacity) {
     // overflow-conscIoUs code
    //判断所需要的容量是否小于char[]数组的容量
    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;//扩容数组大小,也就是原来长度*2+2
   if (newCapacity - minCapacity < 0) {
       newCapacity = minCapacity;
   }
   return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
       ? hugeCapacity(minCapacity)
       : newCapacity;
}

因此,如果再一次追加的时候容量足够,就无需创建新数组,也就省去了很多创建char[]的次数.

小结:

  • String之所以慢是因为,大部分cpu资源都被浪费在分配资源拷贝资源的部分了,相比StringBuilder有更多的内存消耗。

  • StringBuilder快就快在,相比String,他在运算的时候分配内存次数小,所以拷贝次数和内存占用也随之减少,当有大量字符串拼接时,StringBuilder创建char[]的次数会少很多。

  • 由于GC的机制,即使原来的char[]没有引用了,那么也得等到GC触发的时候才能回收,String运算过多的时候就会产生大量垃圾,消耗内存。

因此:

  • 如果目标字符串需要大量拼接的操作,那么这个时候应当使用StringBuilder.

  • 反之,如果目标字符串操作次数极少,或者是常量,那么就直接使用String.

String.intern()

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,则放入成功

  • 如果有该字符串对象,则放入失败

无论放入是否成功,都会返回串池中的字符串对象

注意:此时如果调用intern方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象

例1

//"a" "b" 被放入串池中,str则存在于堆内存之中
String str = new String("a") + new String("b");
//调用str的intern方法,这时串池中没有"ab",则会将该字符串对象放入到串池中,此时堆内存与串池中的"ab"是同一个对象
String st2 = str.intern();
//给str3赋值,因为此时串池中已有"ab",则直接将串池中的内容返回
String str3 = "ab";
//因为堆内存与串池中的"ab"是同一个对象,所以以下两条语句打印的都为true
System.out.println(str == st2);
System.out.println(str == str3);

例2

//此处创建字符串对象"ab",因为串池中还没有"ab",所以将其放入串池中
String str3 = "ab";
//"a" "b" 被放入串池中,str则存在于堆内存之中
String str = new String("a") + new String("b");
//此时因为在创建str3时,"ab"已存在于串池中,所以放入失败,但是会返回串池中的"ab"
String str2 = str.intern();
//false,str在堆内存,str2在串池
System.out.println(str == str2);
//false,str在堆内存,str3在串池
System.out.println(str == str3);
//true,str2和str3是串池中的同一个对象
System.out.println(str2 == str3);

关于作者

来自一线程序员Seven的探索与实践,持续学习迭代中~

本文已收录于我的个人博客https://www.seven97.top

公众号:seven97,欢迎关注~

相关文章

Java中的String是不可变对象 在面向对象及函数编程语言中,不...
String, StringBuffer 和 StringBuilder 可变性 String不可变...
序列化:把对象转换为字节序列的过程称为对象的序列化. 反序...
先说结论,是对象!可以继续往下看 数组是不是对象 什么是对...
为什么浮点数 float 或 double 运算的时候会有精度丢失的风险...
面试题引入 这里引申出一个经典问题,看下面代码 Integer a ...