JavaStringStringBuilder和StringBuffer的区别

Posted remo0x

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaStringStringBuilder和StringBuffer的区别相关的知识,希望对你有一定的参考价值。

三者关系

String实现了Serializable、Comparable、CharSequence接口

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence

StringBuilder继承了AbstractStringBuilder类,实现了Serializable、CharSequence接口

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

StringBuffer继承了AbstractStringBuilder类,实现了Serializable、CharSequence接口

public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence

三者的继承关系如下所示

Object
   |——>String
   |——>AbstractStringBuilder
           |——>StringBuilder
           |——>StringBuffer

StringBuilder和StringBuffer的关系最近,而String是单独实现的。StringBuilder和StringBuffer中的方法几乎都是调用AbstractStringBuilder中的方法实现的,它们有同样的方法,只是StringBuffer在方法上加上了synchronized修饰。StringBuilder用于单线程,StringBuffer用于多线程,而String由于是final的所以也是线程安全的

由于StringBuilder和StringBuffer几乎只是代理了AbstractStringBuilder中的数据和方法,所以只需要分析AbstractStringBuilder就行了

CharSequence

三者都实现了CharSequence接口,所以有一些共同的方法。主要的方法如下所示

方法描述
int length()返回字符序列的长度,按2字节char计算
char charAt(int index)返回指定索引号的char
CharSequence subSequence(int start,int end)返回子字符序列,长度为(end-start)

常用的也就length()和charAt(),而这两个方法都是CharSequence中声明的

底层实现

String是用final字符数组实现的,所以value.length就是真实数据的长度,也是length()的返回值。而String的hash值也因此相当于是固定的,所以可以缓存在变量中

private final char value[];
private int hash;

AbstractStringBuilder是用可变字符数组实现的,所以需要用一个count记录数组中的真实数据

char[] value;
int count;

StringBuffer中比StringBuilder多出了一个字段toStringCache,只要修改了value就更新该字段

private transient char[] toStringCache;

而该字段只在toString()中使用

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

toString()调用的String的构造函数如下

String(char[] value, boolean share) 
    // assert share : "unshared not supported";
    this.value = value;

我猜测增加这个字段应该是这样考虑的

  • StringBuffer用于多线程,所以增加一个toString()的缓存可以减少调用Arrays.copyOfRange()的次数,减少了复制开销
  • 在toString()中调用的String的构造函数是共享toStringCache生成的String对象,减少了复制操作而增加了执行速度。由于String不可修改,所以不能由该String对象影响toStringCache的值,这是安全的

构造函数

String的构造函数的基本思路都是复制源char数组生成一个新的char数组,如果不提供源数组则新建一个char[0]数组,比较典型的如下

public String() 
    this.value = new char[0];


public String(String original) 
    this.value = original.value;
    this.hash = original.hash;


public String(char value[]) 
    this.value = Arrays.copyOf(value, value.length);

AbstractStringBuilder只有一个构造函数,使用指定的capacity新建数组

AbstractStringBuilder(int capacity) 
    value = new char[capacity];

在StringBuilder和StringBuffer中会调用AbstractStringBuilder的构造函数新建对象,基本思路是使用指定的capacity新建数组,如果没有提供capacity则使用16,如果提供了源char数组则将容量扩大16。比如StringBuilder的构造函数

public StringBuilder() 
    super(16);


public StringBuilder(int capacity) 
    super(capacity);


public StringBuilder(String str) 
    super(str.length() + 16);
    append(str);

equals和hashCode()

AbstractStringBuilder是可变的,所以没有提供equals方法和hashCode(),也没有实现Comparable接口。如果要比较AbstractStringBuilder,需要调用toString()将其转为String对象再进行比较操作

String的equals方法是比较value中的字符是否相等

public boolean equals(Object anObject) 
    if (this == anObject) 
        return true;
    
    if (anObject instanceof String) 
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) 
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) 
                if (v1[i] != v2[i])
                    return false;
                i++;
            
            return true;
        
    
    return false;

equals方法判断相等的步骤是:

  • 若指向同一个内存地址,即为同一个String实例,返回true
  • 如果对象不是String实例,返回false
  • 如果value长度不一样,返回false
  • 逐个字符比较,若有不相等字符返回false
  • 所有字符都相同,返回true

String的hashCode()是根据value生成hash值,这样不同value的字符串就容易生成不同的hash值,既能减少碰撞又能生成与内容相关的hash值

public int hashCode() 
    int h = hash;
    if (h == 0 && value.length > 0) 
        char val[] = value;

        for (int i = 0; i < value.length; i++) 
            h = 31 * h + val[i];
        
        hash = h;
    
    return h;

实现的计算表达式如下所示(s[i]是字符串的第i个字符,n是字符串的长度,^表示求幂。空字符串的hash值是0):

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

对于乘子31的选择,主要有两点原因:

  • 31是一个不大不小的质数,是作为hashCode()乘子的优选质数之一,可以均匀分布hash值并减少信息丢失
  • 31可以被JVM优化,31 * i = (i << 5) - i

String方法

String不可变,所以没有提供append方法和insert方法,某些看起来是修改String的方法其实都是new了一个新的String返回。比如replace方法

public String replace(char oldChar, char newChar) 
    if (oldChar != newChar) 
        int len = value.length;
        int i = -1;
        char[] val = value; /* avoid getfield opcode */

        // 找到第一个指向oldChar的i
        while (++i < len) 
            if (val[i] == oldChar) 
                break;
            
        
        if (i < len) 
            char buf[] = new char[len];
            // 将i之前的char复制到buf中
            for (int j = 0; j < i; j++) 
                buf[j] = val[j];
            
            // 将所有oldChar换成newChar
            while (i < len) 
                char c = val[i];
                buf[i] = (c == oldChar) ? newChar : c;
                i++;
            
            return new String(buf, true);
        
    
    return this;

AbstractStringBuilder方法

AbstractStringBuilder主要是提供了append方法和insert方法及其重载,用于便捷的修改char数组,最终都是在value上进行操作

比如append(String str)

public AbstractStringBuilder append(String str) 
    if (str == null)
        return appendNull();
    int len = str.length();
    // 先扩展value,再copy进去,这样可以返回this
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;

还有insert方法

public AbstractStringBuilder insert(int index, char[] str, int offset,
                                        int len)
    if ((index < 0) || (index > length()))
        throw new StringIndexOutOfBoundsException(index);
    if ((offset < 0) || (len < 0) || (offset > str.length - len))
        throw new StringIndexOutOfBoundsException(
            "offset " + offset + ", len " + len + ", str.length "
            + str.length);
    ensureCapacityInternal(count + len);
    System.arraycopy(value, index, value, index + len, count - index);
    System.arraycopy(str, offset, value, index, len);
    count += len;
    return this;

AbstractStringBuilder可以自动扩展value的大小,由下面的几个方法实现

public void ensureCapacity(int minimumCapacity) 
    if (minimumCapacity > 0)
        ensureCapacityInternal(minimumCapacity);


private void ensureCapacityInternal(int minimumCapacity) 
    // overflow-conscious code
    if (minimumCapacity - value.length > 0)
        expandCapacity(minimumCapacity);


void expandCapacity(int minimumCapacity) 
    int newCapacity = value.length * 2 + 2;
    // 取两者最大值
    if (newCapacity - minimumCapacity < 0)
        newCapacity = minimumCapacity;
    if (newCapacity < 0) 
        if (minimumCapacity < 0) // overflow
            throw new OutOfMemoryError();
        // (value.length * 2 + 2)溢出
        newCapacity = Integer.MAX_VALUE;
    
    value = Arrays.copyOf(value, newCapacity);

执行速度

理论上讲,三者的执行速度如下

String < StringBuffer < StringBuilder

因为String不可变所以对它的操作会产生一个新的String对象,所以速度最慢。而StringBuffer在StringBuilder的基础上加上了synchronized,所以速度比StringBuilder慢

但是如果把StringBuffer用于单线程中,那JVM可能会删除synchronized修饰用于性能调优,这时候StringBuilder和StringBuffer几乎是一样的

在某些特殊情况下, String对象的字符串拼接被JVM编译成StringBuilder对象的拼接,所以这些时候String对象的速度并不会比StringBuffer对象慢,比如

String a = "1" + "2" + "3";
# 编译成
String a = new StringBuilder("1").append("2").append("3").toString();

参考文章

以上是关于JavaStringStringBuilder和StringBuffer的区别的主要内容,如果未能解决你的问题,请参考以下文章

tokenize($s) 和 tokenize($s, ' ') 一样吗?

C/S架构和B/S架构

SSG和s-s-r如何选择?

C/S和B/S两种架构的概念区别和联系

CORS 和 s-s-r 以及 express、react 和 Axios

APUE:文件和目录