Java StringBuilder源码剖析+面试题整理

在String中提到,如果字符串修改操作比较频繁,应该采用StringBuilder和StringBuffer类,这两个类的方法基本是完全一样的,它们的实现代码也几乎一样,唯一的不同就在于StringBuffer类是线程安全的,而StringBuilder类不是。

基本用法

创建StringBuilder对象:

StringBuilder sb = new StringBuilder();

通过append方法添加字符串:

sb.append("Hello");
sb.append(" World") ;

通过toString方法获取构建后的字符串:

System.out.println(sb.toString());

实现原理

创建

与String类似,StringBuilder类也封装了一个字符数组,定义如下:

char[] value;
int count;

与String不同,它不是final的,可以修改。另外,与String不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量count,表示数组中已经使用的字符个数。

StringBuilder继承自AbstractStringBuilder,它的默认构造方法是:

public StringBuilder() {
       super(16);
}

其中调用父类的构造方法,父类对应的构造方法是:

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

也就是说,new StringBuilder()代码内部会创建一个长度为16的字符数组,count的默认值为0。

append

append方法的代码:

public AbstractStringBuilder append(String str) {
       if(str == null) str = "null";
       int len = str.length();
       ensureCapacityInternal(count + len);
       str.getChars(0, len, value, count);
       count += len;
       return this;
   }

append会直接复制字符到内部的字符数组中,如果字符数组长度不够,会进行扩展,实际使用的长度用count体现。具体来说,ensureCapacityInternal(count+len)会确保数组的长度足以容纳新添加的字符,str.getChars会复制新添加的字符到字符数组中,count+=len会增加实际使用的长度。

ensureCapacityInternal的代码如下:

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

如果字符数组的长度小于需要的长度,则调用expandCapacity进行扩展,其代码为:

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的扩容是靠Arrays实现的,所以原理也一样。

这里的扩展策略是跟当前长度相关的,当前长度乘以2,再加上2,如果这个长度不够最小需要的长度,才用minimumCapacity。比如,默认长度为16,长度不够时,会先扩展到16*2+2即34,然后扩展到34*2+2即70,然后是70*2+2即142,这是一种指数扩展策略。为什么要加2?这样,在原长度为0时也可以一样工作。为什么要这么扩展呢?这是一种折中策略,一方面要减少内存分配的次数,另一方面要避免空间浪费。在不知道最终需要多长的情况下,指数扩展是一种常见的策略,广泛应用于各种内存分配相关的计算机程序中。不过,如果预先就知道需要多长,那么可以调用StringBuilder的另外一个构造方法:public StringBuilder(int capacity)

toString

toString方法的代码:

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

基于内部数组新建了一个String。注意,这个String构造方法不会直接用value数组,而会新建一个,以保证String的不可变性。

insert

insert是在指定索引offset处插入字符串str

public AbstractStringBuilder insert(int offset, String str) {
       if((offset < 0) || (offset > length()))
           throw new StringIndexOutOfBoundsException(offset);
       if(str == null)
           str = "null";
       int len = str.length();
       ensureCapacityInternal(count + len);
       System.arraycopy(value, offset, value, offset + len, count - offset);
       str.getChars(value, offset);
       count += len;
       return this;
}

实现思路是:在确保有足够长度后,首先将原数组中offset开始的内容向后挪动n个位置,n为待
插入字符串的长度,然后将待插入字符串复制进offset位置。

挪动位置调用了System.arraycopy()方法,将数组src中srcPos开始的length个元素复制到数组dest中destPos处。这个方法有个优点:即使src和ldest是同一个数组,它也可以正确处理。

public static native void arraycopy(Object src, int srcPos,Object dest, int destPos, int length);

+运算

Java中,String可以直接使用+和+=运算符,这是Java编译器提供的支持,背后,Java编译器一般会生成StringBuilder,+和+=操作会转换为append。比如,如下代码:

String hello = "hello";
hello+=",world";
System.out.println(hello);

背后,Java编译器一般会转换为:

StringBuilder hello = new StringBuilder("hello");
hello.append(", world");
System.out.println(hello.toString());

不过,在稍微复杂的情况下,Java编译器可能没有那么智能,它可能会生成过多的StringBuilder,尤其是在有循环的情况下,在循环内部,每一次+=操作,都会生成一个StringBuilder。所以,对于简单的情况,可以直接使用String的+和+=,对于复杂的情况,尤其是有循环的时候,应该直接使用StringBuilder。

八股考点

String、StringBuffer、StringBuilder 的区别?

String:用于字符串操作,属于不可变类;【补充:String不是基本数据类型,是引用类型,底层用char数组实现的】
StringBuilder:与StringBuffer类似,都是字符串缓冲区,但线程不安全;
StringBuffer:也用于字符串操作,不同之处是StringBuffer 属于可变类,对方法加了同步锁,线程安全(补充说明:StringBuffer中并不是所有方法都使用了Synchronized修饰来实现)

执行效率:StringBuilder>StringBuffer>String

String中“+”和StringBuffer中的 append会有性能上的差别吗?

会有性能上的差别。
在Java中,String对象是不可变的,也就是说每次使用"+“运算符连接字符串时,都会创建一个新的String对象,这就涉及到内存分配和垃圾回收,如果在循环或频繁操作中使用”+"进行字符串拼接,会大大降低性能。
而StringBuffer和StringBuilder的对象是可变的,append方法实际上是在原有的字符序列后面添加字符,不会创建新的对象,所以在进行大量或复杂的字符串操作时,使用StringBuffer或StringBuilder的性能要高于使用String的"+"。
另外,StringBuffer是线程安全的,而StringBuilder是非线程安全的,所以在单线程环境下,StringBuilder的性能会更高一些。

final修饰 StringBuffer后还可以 append 吗?

可以。final修饰的是一个引用变量,那么这个引用始终只能指向这个对象,但是这个对象内部的属性是可以变化的。

你可能感兴趣的:(Java常用类的源码剖析,java,面试,开发语言)