1、前言
相信学习Java的小伙伴对String、StringBuilder、StringBuffer都不会陌生,几乎每天都和它们打交道,但是你是否真的了解其它们呢?虽然看似简单的基础知识,但是确实高频的面试点。很多同学背过相关面试题,但是很容易就会忘记,主要是没有只有背、少了理解,因此很容易就会给忘记了。
2、StringBuilder的简单使用
StringBuilder sb=new StringBuilder();
sb.append("hello");
sb.append("world");
System.out.println(sb.toString());
是不是非常的熟悉,StringBuilder可以用来拼接字符串。那么大家有没有相关它底层是如何实现的呢?
3、类的关系梳理
第一,定义一个接口
public interface CharSequence {
//获取内容长度
int length();
//定位某个字符
char charAt(int index);
//截取字符串
CharSequence subSequence(int start, int end);
//toString
public String toString();
}
第二,定义一个抽象类
abstract class AbstractStringBuilder implements Appendable, CharSequence {
//数组字符的数组
char[] value;
//字符长度
int count;
//构造函数(传递容量大小)
AbstractStringBuilder(int capacity) {
//初始化一个数组
value = new char[capacity];
}
//返回长度
public int length() {
return count;
}
//返回容量大小
public int capacity() {
return value.length;
}
}
第三步,定义一个实现类
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
//构造函数
public StringBuilder() {
//如果不指定容量值,则默认是16
super(16);
}
//构造函数
public StringBuilder(int capacity) {
super(capacity);
}
}
通过以上的简单类结构,我们需要掌握以下几个核心问题
①什么需要加一个抽象类,AbstractStringBuilder呢?为什么不直接实现类实现接口呢?
②StringBuilder底层是char[]数组,它的数据结构是数组,如果不指定则默认是16
③为什么需要count字段呢?length()方法和capacity()的区别是什么呢?这不是多余的吗?
温馨提示:我们看源码的时候,看的过程带着问题、带着疑问去源码当中寻找答案,这样才能有所收获,而不是为了看源码而去看源码。
一般来说,抽象类可以用来解耦接口和实现类,很多优秀的框架几乎都是这种模式,好处是把公共的部分抽取到抽象类实现,减轻实现类的操作,具体如下所示。
//接口
public interface ITest{
public void sayHello(String name);
}
//实现类
public class TestImpl implements ITest{
@Override
public void sayHello(String name){
//对name进行校验
if(name!=null&&!"".equals(name)){
//具体的业务处理
}
}
}
这种模式的缺点就是如果ITest有好多个实现类,每个实现类都需要对name字段进行校验。那么如何优化呢?
//接口
public interface ITest{
public void sayHello(String name);
}
//抽象类
public class AbstractTestImpl implements ITest{
@Override
public void sayHello(String name){
//对name进行校验
if(name!=null&&!"".equals(name)){
//调用抽象方法
say(name);
}
}
//定义一个抽象方法
public abstract void say(String name);
}
//实现类
public class TestImpl extends AbstractTestImpl{
public void say(String name){
//无需要做校验,直接开始处理业务即可
}
}
看到这里,是不是明白为什么需要加AbstractStringBuilder类了呢?
4、StringBuilder存储数据
AbstractStringBuilder类的append方法解析
public AbstractStringBuilder append(String str) {
if (str == null){
str = "null";
}
//1.获取追加字符串的长度
int len = str.length();
//2.动态扩容char[]数组【数组长度是count+len】【继续看源码】
ensureCapacityInternal(count + len);
//3.往扩容char[]数组添加内容
str.getChars(0, len, value, count);
//4.count属性累加
count += len;
return this;
}
StringBuilder类的append方法解析
public StringBuilder append(String str) {
//它本身不处理,只是调用父类的方法
super.append(str);
return this;
}
那么如何扩容的呢?
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
}
void expandCapacity(int minimumCapacity) {
//1.新数组的长度是原来的两倍
int newCapacity = value.length * 2 + 2;
//2.判断两个值,最后觉得以哪个长度为主
if (newCapacity - minimumCapacity < 0){
newCapacity = minimumCapacity;
}
//3.判断新数组长度是否为0
if (newCapacity < 0) {
if (minimumCapacity < 0){
throw new OutOfMemoryError();
}
//如果小于0则取Integer的最大值
newCapacity = Integer.MAX_VALUE;
}
//4.拷贝数组【继续看源码】
value = Arrays.copyOf(value, newCapacity);
}
思考:newCapacity为什么会小于0呢?是不是很奇怪呢?
解析:因为int是32位的二进制,最高位是符号位(0正数,1负数),如果newCapacity大于Integer最大值,那么首位被挤掉了,由0变成1,那么就编程了负数了。
public static char[] copyOf(char[] original, int newLength) {
//1.创建一个新的数组
char[] copy = new char[newLength];
//2.拷贝数组【继续看源码】
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
System.arraycopy是JVM底层提供的方法,native修饰的,用它来进行数组之间的拷贝。
源码分析到这里,我们必须掌握两个核心的东西
第一)StringBuilder底层是char[]数组
第二)StringBuilder是动态扩容的,它是通过创建一个新的数组,然后把旧数组的数据拷贝到新数组当中,旧数组给gc回收
5、StringBuilder删除数据
public AbstractStringBuilder deleteCharAt(int index) {
//1.校验
if ((index < 0) || (index >= count)){
throw new StringIndexOutOfBoundsException(index);
}
//2.拷贝数组【同一个数组之间的拷贝】
System.arraycopy(value, index+1, value, index, count-index-1);
//3.count递减
count--;
return this;
}
拷贝这个地方思路可能有点绕,给大家懂点分析一下
char[] arrs={0,1,2,3,4,5,6},有7个元素,我们要删除index=4的元素,那么如何删除呢?思路是arrs数组直接的拷贝,
System.arraycopy(src,srcPos,dest,destPos,length)
①src表示源数组
②srcPos表示从源数组的什么位置开始拷贝
③dest表示拷贝到哪个数组
④destPos表示拷贝到新数组的哪个位置
⑤length表示拷贝旧数组的几个元素
System.arraycopy(arrs,index+1,arrs,index,arrs.length-1-index)会执行如下操作
①0,1,2,3元素不变
②第5个元素放的是5
③第6个元素放的是6
④第七个元素是4,4会被挤到最后【大家可以测试该函数的使用】
最后结果是,char[] arrs={0,1,2,3,5,6,4}
4就是被我们删除的元素,我们只能通过把它挤到最后,然后通过count–表示数组的有效长度,那么读取数组的时候不是
for(int i=0;i<arrs.length;i++),而是for(int i=0;i<count;i++)
因此,大家回过头去看前面的问题,为什么需要count字段,length()和capacity()方法的区别,是不是就恍然大悟了。
6、总结
看到这里StringBuilder的核心思路就讲解完成了,它有很多的方法,大家可以自己去看,其实都不难,掌握核心思想即可。
第一)它底层是char[]数组
第二)新增元素的时候,如何扩容数组
第三)删除元素的时候,又是如何处理的
慕课网专栏(架构思想之微服务+仿百度网盘源码):
https://www.imooc.com/read/73
感谢您的阅读,希望您多多支持,这样我在原创的道路上才能更加的有信心。