深入理解Java常用类-----StringBuilder

时间:2024-02-15 21:51:22

     上篇文章我们介绍过String这个常用类,知道了该类的内部其实是用的一个char数组表示一个字符串对象的,只是该字符数组被final修饰,一旦初始化就不能修改,但是对于经常做字符串修改操作的情况下,String类就需要不断创建新对象,性能极低。StringBuilder内部也是封装的一个字符数组,只不过该数组非final修饰,可以不断修改。所以对于一些经常需要修改字符串的情况,我们应当首选StringBuilder。其实StringBuilder和StringBuffer内部代码几乎一样,只是StringBuffer的所有方法都被关键字synchronized修饰,也就是说它是线程安全的,但是线程安全是需要付出性能代价的,所以在实际使用中,适情况选择。本篇主要介绍StringBuilder,以下是本篇主要内容:

  • 强大的父类AbstractStringBuilder
  • 多重载的构造函数
  • 重要的append方法
  • 其他一些方法的简单介绍

一、强大的父类AbstractStringBuilder
     StringBuilder的大部分方法中都会调用父类方法或属性, 足以见得该父类对其的影响还是很大的,所以我们将从头至尾简单介绍下它的父类AbstractStringBuilder。该类中只有两个属性:

//The value is used for character storage.
char[] value;
//The count is the number of characters used.
int count;

value属性表示的是一个字符数组,该数组的作用和String中的字符数组的作用是一样的,只是此value数组并没有被final修饰,也就是说该数组内部的值是可以动态修改的,这也是StringBuilder存在的意义。count属性表示的不是value数组的长度,它代表的是value数组中实际上存放的字符数目,例如:value长度为10,我存放8个字符,剩下位置为空,此时count的值就为8,而value.length()为10。

两个构造方法都不是public,他们都是被设计出来给子类使用的。

AbstractStringBuilder() {}

//初始化value
AbstractStringBuilder(int capacity) {
     value = new char[capacity];
}

还有两个用于返回长度的方法:

public int length() {
    return count;
}
public int capacity() {
    return value.length;
}

一个返回的是实际存放的字符数目,另一个返回的是内置字符数组的长度。
还有一个用于保证字符数组长度的方法,该方法和我们之前介绍的ArrayList中用于动态扩容的方法具有一样的功效。

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();
        newCapacity = Integer.MAX_VALUE;
    }
    value = Arrays.copyOf(value, newCapacity);
}

对于一个StringBuilder对象,我们可以不断的追加字符串到其中,这样就会遇到value数组长度不够的时候,该方法就是用于处理这种情况,在我们实际操作value数组之前,大多会调用该方法判断此次操作之后是否会导致数组溢出,如果是则会将原数组长度扩大两倍加上2并拷贝原数组中的内容到新数组中,然后才实际操作value数组。(此时的value数组已经被扩容了)。

这种动态扩容的思想还被用于ArrayList中,成为它和普通数组的核心优势。这种思想其实是一种折中的解决方案,它既避免了一次性创建很大的数组所导致的资源浪费,也解决了那种一旦创建就不能更改的静态局限性。这种思想值得我们学习和应用。

该类还提供一种方法,去除value数组中所有为空的元素:

public void trimToSize() {
   if (count < value.length) {
       value = Arrays.copyOf(value, count);
   }
}

看个例子:

public static void main(String[] args){
    StringBuilder sb = new StringBuilder();
    sb.append("hello");
    System.out.println(sb.length());
    System.out.println(sb.capacity());

    sb.trimToSize();
    System.out.println(sb.length());
    System.out.println(sb.capacity());
}

我们看输出结果:

5
16
5
5

需要解释下,没有显式指定value的长度,则会默认为16,程序为value的前5个位置赋值,后面的位置为空,所以我们看到第一次输出结果是有区别的。但是我们的trimToSize方法把没有使用的空位置全部清除,第二次输出结果显示capcity和length是一样的。

该类中其他的一些方法,例如:getChars,charAt等,这些方法都是很String类中对应的方法具有一样功效。下面我们主要看看一个重要的方法,append。该方法也具有相当多的重载,所以我们慢慢看。

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;
}

append这个方法是我们使用StringBuilder时最常用到的一个方法,该方法用于追加一个字符串到原StringBuilder对象的尾部。该方法接过来一个String对象,如果为null将会调用appendNull方法把字符串“null”追加到原对象的末尾,否则将会把该字符串追加到原对象的末尾。

其他重载都是以各种各样的形式添加字符串到原StringBuilder对象的末尾,如果传入的是int,long,double,boolean等类型的参数,那么程序会将他们转换为字符串类型添加到末尾。我们也可以指定添加一个字符串的一部分,例如:

public AbstractStringBuilder append(char str[], int offset, int len)

至此,我们就已经完成AbstractStringBuilder这个超类的简单介绍,至于其中还有一些其他方法,我们将从他的实现类StringBuilder中继续介绍。下面我们看看有关StringBuilder类的一些常用方法的介绍。

二、多重载的构造函数
     StringBuilder除了封装了一个版本号,并没有封装任何其他的属性,甚至没有封装字符数组,那是因为它高度依赖他的父类,使用的是父类中封装的字符数组。包括他的构造函数也是调用的父类中的构造函数,例如:

public StringBuilder() {
    super(16);
}
public StringBuilder(int capacity) {
    super(capacity);
}
public StringBuilder(String str) {
    super(str.length() + 16);
    append(str);
}

这些构造函数会调用父类的一个构造函数为value字符数组初始化长度,如果没有显式传入需要设定的数组长度,则会默认为16,这一点我们在之前的实例中曾演示过。这些构造器主要完成的工作就是初始化一个字符数组。下面我们看StringBuilder的一个重要方法:append。

三、重要的append方法
     StringBuilder中的append方法都是重写了父类中的append方法:

@Override
public StringBuilder append(boolean b) {
    super.append(b);
    return this;
}

@Override
public StringBuilder append(char c) {
    super.append(c);
    return this;
}

@Override
public StringBuilder append(int i) {
    super.append(i);
    return this;
}

@Override
public StringBuilder append(long lng) {
    super.append(lng);
    return this;
}

@Override
public StringBuilder append(float f) {
    super.append(f);
    return this;
}

其实别看这么多重载,实际上都是相互调用,真正有用的就一两个。他们这些方法重载内部都会调用父类中相对应的方法,虽然没有接受返回值,但是父类方法完成了对value数组的赋值操作,最后调用完成append方法之后返回的是StringBuilder对象,这意味着我们可以连续调用append方法。

其实我们需要始终明白一点,StringBuilder和StringBuffer他们其实和String差不多,内部一样都是封装的字符数组,只不过StringBuilder实现了动态扩容机制,可以动态扩容并且可以动态更改value数组中的元素而已,但本质上都是一样的。

四、有关StringBuilder的一些其他使用细节
     首先我们看一个删除的方法,该方法可以指定删除StringBuilder对象中指范围内的子串。

public AbstractStringBuilder delete(int start, int end) {
    if (start < 0)
        throw new StringIndexOutOfBoundsException(start);
    if (end > count)
        end = count;
    if (start > end)
        throw new StringIndexOutOfBoundsException();
    int len = end - start;
    if (len > 0) {
        System.arraycopy(value, start+len, value, start, count-end);
        count -= len;
    }
    return this;
}

该方法中最核心的方法是System.arraycopy,这个方法也被广泛使用在数组拷贝转移中。该方法将value数组索引位置为start+len开始以后的所有字符向前移动到start索引位置起,start+count-len终止位置处。需要记住的是,该方法并没有创建一个新数组,而是对原value数组进行移动元素来实现的。实际上该方法就是将即将被删除的子串后面的所有字符整体移动到被删除子串的开始位置。

这里写图片描述

value数组长度没有发生改变,只是用后面的子串覆盖了将要被删除的子串,然后count -= len;更新count,但是实际上并没有删除。但是count的值指定了该value数组中有效的字符数目,虽然没有具体删除该元素,但是在输出的时候只会把前count个字符作为有效字符输出。这就是delete的底层操作,包括其中的deleteCharAt删除指定位置的字符,原理都是一样的。

该类中还有一些replace,substring等方法,这些方法和我们之前曾介绍过的String类中相对应的方法底层实现都是类似,此处不再赘述了。下面我们看一个String类中没有的方法,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;
}

有了之前delete方法学习的基础之后,这个插入的方法就很简单了。该方法接受四个参数,第一个参数表示要插入的索引位置,第二个参数表示要插入的字符数组或者字符串,第三个参数和第四个参数用于截取该原字符数组。核心方法依然是System.arraycopy,不过这里调用了两次,第一次的调用将index位置之后的所有字符往后移动len个长度(为即将插入的字符串留下空位置),第二次调用将该字符数组插入到预留位置。

该insert方法有很多重载,但是本质上都离不开我们上述介绍的这个方法。所以为了借阅篇幅,此处不再赘述。

至此,我们就简单介绍完成了StringBuilder的基本使用,理解不到之处,望大家指出,相互学习!