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

     上篇文章我们介绍过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的基本使用,理解不到之处,望大家指出,相互学习!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏电光石火

HashSet集合中hashCode及equals方法详解

首先我们熟知HashSet集合中元素存储的特点: 1)不允许元素重复; 2)不会记录元素添加的先后顺序; 3)HashSet中比较两个对象是否相同...

1679
来自专栏智能算法

Python学习(五)---- 不可不知的装饰器!

https://blog.csdn.net/fgf00/article/details/52061971

502
来自专栏海天一树

小朋友学C语言(7):自定义函数

先动手编写程序: #include <stdio.h> int add(int x, int y) { int z = x + y; retur...

3215
来自专栏Phoenix的Android之旅

Kotlin的循环控制

所有的计算机程序总结起来只干了三件事情,顺序,条件,循环。 在Java中可以用 break, continue, return来进行循环控制,Kotlin中也是...

241
来自专栏C语言C++游戏编程

轻松学习C语言编程之函数知识详解

函数是一组一起执行任务的语句。每个C程序至少有一个函数,即main,所有最简单的程序都可以定义其他函数。您可以将代码划分为单独的函数。如何在不同的函数之间划分代...

642
来自专栏Coding迪斯尼

开发自制语言Monkey编译器:实现复杂算术表达式的执行

1044
来自专栏IMWeb前端团队

那些出乎意料的类型转换

本文作者:IMWeb helinjiang 原文出处:IMWeb社区 未经同意,禁止转载 JavaScript是一门弱类型的语言,因此类型之间的转换会...

1956
来自专栏诸葛青云的专栏

C语言入门基础学习函数?来看我就告诉你!

在前面我们已经讲过了一些简单的函数,如程序的主函数main()、标准输出函数printf()。在C语言中,大多数功能都是依靠函数来实现的。But,你知道什么是函...

813
来自专栏老九学堂

Java微课堂-运算符

Java运算符微视频笔记 赋值运算符 这讲的知识点不多,重点是大家要理解运算符的作用,运算符往往是和变量结合使用,用来解决一些常用的逻辑。比如赋值运算符就是给变...

3267
来自专栏强仔仔

java中返回任意类型值( <V> V get(Object obj))

今天给大家介绍一下java中是如何实现返回值为任何类型,而且不需要强制类型转换就可以直接使用。 在一般情况下返回类型要么是范型,要么就是引用类型、基础类型之类的...

19010

扫码关注云+社区