推进门户网站建设工作,wordpress 删除所有文章,郑州妇科医院免费咨询,wordpress素才前言在日常的开发中StringBuilder大家肯定都有用过#xff0c;甚至用的很多。毕竟大家都知道一个不成文的规范#xff0c;当需要高频的大量的构建字符串的时候StringBuilder的性能是要高于直接对字符串进行拼接的#xff0c;因为直接使用或都会产生一个新的String实例#… 前言 在日常的开发中StringBuilder大家肯定都有用过甚至用的很多。毕竟大家都知道一个不成文的规范当需要高频的大量的构建字符串的时候StringBuilder的性能是要高于直接对字符串进行拼接的因为直接使用或都会产生一个新的String实例因为String对象是不可变的对象这也就意味着每次对字符串内容进行操作的时候都会产生一个新的字符串实例这对大量的进行字符串拼接的场景是非常不友好的。因此StringBuilder孕育而出。这里需要注意的是这并不意味着可以用StringBuilder来代替所有字符串拼接的的场景这里我们强调一下是频繁的对同一个字符串对象进行拼接的操作。今天我们就来看一下c#中StringBuilder的巧妙实现方式体会一下底层类库解决问题的方式。需要注意的是这里的不可变指的是字符串对象本身的内容是不可改变的,但是字符串变量的引用是可以改变的。简单示例接下来咱们就来简单的示例一下操作其实核心操作主要是Append方法和ToString方法源码的的角度上来说还有StringBuilder的构造函数。首先是大家最常用的方式直接各种Append然后最后得到结果。StringBuilder builder new StringBuilder();
builder.Append(我和我的祖国);
builder.Append(,);
builder.Append(一刻也不能分割);
builder.Append(。);
builder.Append(无论我走到哪里,都留下一首赞歌。);
builder.Append(我歌唱每一座高山,我歌唱每一条河。);
builder.Append(袅袅炊烟,小小村落,路上一道辙。);
builder.Append(我永远紧依着你的心窝,你用你那母亲的脉搏,和我诉说。);
string result builder.ToString();
Console.WriteLine(result);StringBuilder也是支持通过构造函数初始化一些数据的有没有在构造函数传递初始化数据也就意味着不同的初始化逻辑。比如以下操作StringBuilder builder new StringBuilder(我和我的祖国);
//或者是指定StringBuilder的容量这样的话StringBuilder初始可承载字符串的长度是16
builder new StringBuilder(16);因为StringBuilder是基础类库因此看着很简单用起来也很简单而且大家也都经常使用这些操作。源码探究上面咱们简单的演示了StringBuilder的使用方式一般的类似的StringBuilder或者是List这种虽然我没使用的过程中可以不关注容器本身的长度一直去添加元素实际上这些容器的本身内部实现逻辑都包含了一些扩容相关的逻辑。上面咱们提到了一下StringBuilder的核心主要是三个操作也就是通过这三个功能可以呈现出StringBuilder的工作方式和原理。•一个是构造函数因为构造函数包含了初始化的一些逻辑。•其次是Append方法这是StringBuilder进行字符串拼接的核心操作。•最后是将StringBuilder转换成字符串的操作ToString方法这是我们得到拼接字符串的操作。接下来咱们就从这三个相关的方法入手来看一下StringBuilder的核心实现这里我参考的.net版本为v6.0.2。构造入手我们上面提到了StringBuilder的构造函数代表了初始化逻辑大概来看就是默认的构造函数即默认初始化逻辑和自定义一部分构造函数的逻辑主要是的逻辑是决定了StringBuilder容器可容纳字符串的长度。无参构造首先来看一下默认的无参构造函数的实现[点击查看源码[1]]//可承载字符的最大容量,即可以拼接的字符串的长度
internal int m_MaxCapacity;
//承载【拼接字符串的char数组
internal char[] m_ChunkChars;
//默认的容量即默认初始化m_ChunkChars的长度也就是首次扩容触发的长度
internal const int DefaultCapacity 16;
public StringBuilder()
{m_MaxCapacity int.MaxValue;m_ChunkChars new char[DefaultCapacity];
}通过默认的无参构造函数我们可以了解到两点信息•首先是StringBuilder核心存储字符串的容器是char[]字符数组。•默认容器的char[]字符数组声明的长度是16即如果首次StringBuilder容纳的字符个数超过16则触发扩容机制。带参数的构造StringBuilder的有参数的构造函数有好几个如下所示//声明初始化容量即首次扩容触发的长度条件
public StringBuilder(int capacity)
//声明初始化容量和最大容量即可以动态构建字符串的总长度
public StringBuilder(int capacity, int maxCapacity)
//用给定字符串初始化
public StringBuilder(string? value)
//用给定字符串初始化并声明容量
public StringBuilder(string? value, int capacity)
//用一个字符串截取指定长度初始化并声明最大容量
public StringBuilder(string? value, int startIndex, int length, int capacity)虽然构造函数有很多但是大部分都是在调用调用自己的重载方法核心的有参数的构造函数其实就两个咱们分别来看一下首先是指定容量的初始化构造函数[点击查看源码[2]]//可承载字符的最大容量,即可以拼接的字符串的长度
internal int m_MaxCapacity;
//承载【拼接字符串的char数组
internal char[] m_ChunkChars;
//默认的容量即默认初始化m_ChunkChars的长度也就是首次扩容触发的长度
internal const int DefaultCapacity 16;
public StringBuilder(int capacity, int maxCapacity)
{//指定容量不能大于最大容量if (capacity maxCapacity){throw new ArgumentOutOfRangeException(nameof(capacity), SR.ArgumentOutOfRange_Capacity);}//最大容量不能小于1if (maxCapacity 1){throw new ArgumentOutOfRangeException(nameof(maxCapacity), SR.ArgumentOutOfRange_SmallMaxCapacity);}//初始化容量不能小于0if (capacity 0){throw new ArgumentOutOfRangeException(nameof(capacity), SR.Format(SR.ArgumentOutOfRange_MustBePositive, nameof(capacity)));}//如果指定容量等于0则使用默认的容量if (capacity 0){capacity Math.Min(DefaultCapacity, maxCapacity);}//最大容量赋值m_MaxCapacity maxCapacity;//分配指定容量的数组m_ChunkChars GC.AllocateUninitializedArraychar(capacity);
}主要就是对最大容量和初始化容量进行判断和赋值如果制定了初始容量和最大容量则以传递进来的为主。接下来再看一下根据指定字符串来初始化StringBuilder的主要操作[点击查看源码[3]]//可承载字符的最大容量,即可以拼接的字符串的长度
internal int m_MaxCapacity;
//承载【拼接字符串的char数组
internal char[] m_ChunkChars;
//默认的容量即默认初始化m_ChunkChars的长度也就是首次扩容触发的长度
internal const int DefaultCapacity 16;
//当前m_ChunkChars字符数组中已经使用的长度
internal int m_ChunkLength;
public StringBuilder(string? value, int startIndex, int length, int capacity)
{if (capacity 0){throw new ArgumentOutOfRangeException();}if (length 0){throw new ArgumentOutOfRangeException();}if (startIndex 0){throw new ArgumentOutOfRangeException();}//初始化的字符串可以为null,如果为null则只用空字符串即if (value null){value string.Empty;}//基础长度判断,这个逻辑其实已经包含了针对字符串截取的起始位置和接要截取的长度进行判断了if (startIndex value.Length - length){throw new ArgumentOutOfRangeException();}//最大容量是int的最大值即2^31-1m_MaxCapacity int.MaxValue;if (capacity 0){capacity DefaultCapacity;}//虽然传递了默认容量,但是这里依然做了判断,在传递的默认容量和需要存储的字符串容量总取最大值capacity Math.Max(capacity, length);//分配指定容量的数组m_ChunkChars GC.AllocateUninitializedArraychar(capacity);//这里记录了m_ChunkChars固定长度的快中已经被使用的长度m_ChunkLength length;//把传递的字符串指定位置指定长度(即截取操作)copy到m_ChunkChars中value.AsSpan(startIndex, length).CopyTo(m_ChunkChars);
}这个初始化操作主要是截取给定字符串的指定长度存放到ChunkChars用于初始化StringBuilder其中初始化的容量取决于可以截取的长度是否大于指定容量实质是以能够存放截取长度的字符串为主。构造小结通过StringBuilder的构造函数中的逻辑我们可以看到StringBuilder本质存储是在char[],这个字符数组的初始化长度是16,这个长度主要的作用是扩容机制即首次需要进行扩容的时机是当m_ChunkChars长度超过16的时候这个时候原有的m_ChunkChars已经不能承载需要构建的字符串的时候触发扩容。核心方法我们上面看到了StringBuilder相关的初始化代码通过初始化操作我们可以了解到StringBuilder本身的数据结构但是想了解StringBuilder的扩容机制还需要从它的Append方法入手因为只有Append的时候才有机会去判断原有的m_ChunkChars数组长度是否满足存储Append进来的字符串。关于StringBuilder的Append方法有许多重载这里咱们就不逐个列举了但是本质都是一样的。因此咱们就选取咱们最熟悉的和最常用的Append(string? value)方法进行讲解直接找到源码位置[点击查看源码[4]]//承载【拼接字符串的char数组
internal char[] m_ChunkChars;
//当前m_ChunkChars字符数组中已经使用的长度
internal int m_ChunkLength;
public StringBuilder Append(string? value)
{if (value ! null){// 获取当前存储块char[] chunkChars m_ChunkChars;// 获取当前块已使用的长度int chunkLength m_ChunkLength;// 获取传进来的字符的长度int valueLen value.Length;//当前使用的长度 需要Append的长度 当前块的长度 则不需要扩容if (((uint)chunkLength (uint)valueLen) (uint)chunkChars.Length){//判断传进来的字符串长度是否2//如果小于2则只用直接访问位置的方式操作if (valueLen 2){//判断字符串长度0的场景if (valueLen 0){//m_ChunkChars的已使用长度其实就是可以Append新元素的起始位置//直接取value得第0个元素放入m_ChunkChars[可存储的起始位置]chunkChars[chunkLength] value[0];}//其实是判断字符串长度2的场景if (valueLen 1){//因为上面已经取了value第0个元素放入了m_ChunkChars中//现在则取value得第1个元素继续放入chunkLength的下一位置chunkChars[chunkLength 1] value[1];}}else{//如果value的长度大于2则通过操作内存去追加value//获取m_ChunkChars的引用位置,偏移到m_ChunkLength的位置追加valueBuffer.Memmove(ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(chunkChars), chunkLength),ref value.GetRawStringData(),(nuint)valueLen);}//更新以使用长度的值,新的使用长度是当前已使用长度追加进来的字符串长度m_ChunkLength chunkLength valueLen;}else{//走到这里说明进入了扩容逻辑AppendHelper(value);}}return this;
}这一部分逻辑主要展示了未达到扩容条件时候的逻辑其本质就是将Append进来的字符串追加到m_ChunkChars数组里去其中m_ChunkLength代表了当前m_ChunkChars已经使用的长度另一个含义也是代表了下一次Append进来元素存储到m_ChunkLength的起始位置。而扩容的需要的逻辑则进入到了AppendHelper方法中咱们看一下AppendHelper方法的实现[点击查看源码[5]]private void AppendHelper(string value)
{//unsafe也意味着操作不是线程安全的unsafe{//防止垃圾收集器重新定位value变量。//指针操作,string本身是不可变的char数组,所以它的指针是char* fixed (char* valueChars value){//调用了另一个appendAppend(valueChars, value.Length);}}
}这里是获取了传递进来的value指针然后调用了另一个重载的Append方法不过从这段代码中可以得到一个信息这个操作是非线程安全的。我们继续找到另一个Append方法[点击查看源码[6]]public unsafe StringBuilder Append(char* value, int valueCount)
{// value必须有值if (valueCount 0){throw new ArgumentOutOfRangeException();}//新的长度StringBuilder的长度需要追加的字符串长度int newLength Length valueCount;//新的长度不能大于最大容量if (newLength m_MaxCapacity || newLength valueCount){throw new ArgumentOutOfRangeException();}// 新的起始位置需要追加的长度当前使用的长度int newIndex valueCount m_ChunkLength;// 判断当前m_ChunkChars的容量是否够用if (newIndex m_ChunkChars.Length){//够用的话则直接将追加的元素添加到m_ChunkChars中去new ReadOnlySpanchar(value, valueCount).CopyTo(m_ChunkChars.AsSpan(m_ChunkLength));//更新已使用的长度为新的长度m_ChunkLength newIndex;}//当前m_ChunkChars不满足存储则需要扩容else{// 判断当前存储块m_ChunkChars还有多少未存储的位置int firstLength m_ChunkChars.Length - m_ChunkLength;if (firstLength 0){//把需要追加的value中的前firstLength位字符copy到m_ChunkChars中剩余的位置//合理的利用存储空间,截取需要追加的value到m_ChunkChars剩余的位置new ReadOnlySpanchar(value, firstLength).CopyTo(m_ChunkChars.AsSpan(m_ChunkLength));//更新已使用的位置这个时候当前存块m_ChunkChars已经存储满了m_ChunkLength m_ChunkChars.Length;}// 获取value中未放入到m_ChunkChars(因为当前块已经放满)剩余部分起始位置int restLength valueCount - firstLength;//扩展当前存储块即扩容操作ExpandByABlock(restLength);//判断新的存储块是否创建成功Debug.Assert(m_ChunkLength 0, A new block was not created.);// 将value中未放入到m_ChunkChars的剩余部放入扩容后的m_ChunkChars中去new ReadOnlySpanchar(value firstLength, restLength).CopyTo(m_ChunkChars);// 更新当前已使用长度m_ChunkLength restLength;}//一些针对当前StringBuilder的校验操作,和相关逻辑无关不做详细介绍//类似的Debug.Assert(m_ChunkOffset m_ChunkChars.Length m_ChunkOffset, The length of the string is greater than int.MaxValue.);AssertInvariants();return this;
}这里的源代码涉及到了一个StringBuilder的长度问题Length代表着当前StringBuilder对象实际存放的字符长度它的定义如下所示public int Length
{//StringBuilder已存储的长度块的偏移量当前块使用的长度get m_ChunkOffset m_ChunkLength;set{//注意这里是有代码的只是我们暂时省略set逻辑}
}上面源码的这个Append方法其实是另一个重载方法只是Append(string? value)调用了这个逻辑这里可以清晰的看到如果当前存储块满足存储则直接使用。如果当前存储位置不满足存储那么存储空间也不会浪费按照当前存储块的可用存储长度去截取需要Append的字符串的长度放入到这个存储块的剩余位置剩下的存储不下的字符则存储到扩容的新的存储块m_ChunkChars中去这个做法就是为了不浪费存储空间。这一点考虑的非常周到即使要发生扩容那么我当前节点的存储块也一定要填充满保证了存储空间的最大利用。通过上面的Append源码我们自然可看出扩容的逻辑自然也就在ExpandByABlock方法中[点击查看源码[7]]//当前StringBuilder实际存储的总长度
public int Length
{//StringBuilder已存储的长度块的偏移量当前块使用的长度get m_ChunkOffset m_ChunkLength;set{//注意这里是有代码的只是我们暂时省略set逻辑}
}
//当前StringBuilder的总容量
public int Capacity
{get m_ChunkChars.Length m_ChunkOffset;set{//注意这里是有代码的只是我们暂时省略set逻辑}
}//可承载字符的最大容量,即可以拼接的字符串的长度
internal int m_MaxCapacity;
//承载【拼接字符串的char数组
internal char[] m_ChunkChars;
//当前块的最大长度
internal const int MaxChunkSize 8000;
//当前m_ChunkChars字符数组中已经使用的长度
internal int m_ChunkLength;
//存储块的偏移量,用于计算总长度
internal int m_ChunkOffset;
//前一个存储块
internal StringBuilder? m_ChunkPrevious;
private void ExpandByABlock(int minBlockCharCount)
{//当前块m_ChunkChars存储满才进行扩容操作Debug.Assert(Capacity Length, nameof(ExpandByABlock) should only be called when there is no space left.);//minBlockCharCount指的是剩下的需要存储的长度Debug.Assert(minBlockCharCount 0);AssertInvariants();//StringBuilder的总长度不能大于StringBuilder的m_MaxCapacityif ((minBlockCharCount Length) m_MaxCapacity || minBlockCharCount Length minBlockCharCount){throw new ArgumentOutOfRangeException();}//!!!需要扩容块的新长度max(当前追加字符的剩余长度,min(当前StringBuilder长度,8000))int newBlockLength Math.Max(minBlockCharCount, Math.Min(Length, MaxChunkSize));//判断长度是否越界if (m_ChunkOffset m_ChunkLength newBlockLength newBlockLength){throw new OutOfMemoryException();}// 申请一个新的存块长度为newBlockLengthchar[] chunkChars GC.AllocateUninitializedArraychar(newBlockLength);//!!!把当前StringBuilder中的存储块存放到一个新的StringBuilder实例中当前实例的m_ChunkPrevious指向上一个StringBuilder//这里可以看出来扩容的本质是构建节点为StringBuilder的链表m_ChunkPrevious new StringBuilder(this);//偏移量是每次扩容的时候去修改,它的长度就是记录了已使用块的长度,但是不包含当前StringBuilder的存储块//可以理解为偏移量长度-已经存放扩容块的长度m_ChunkOffset m_ChunkLength;//因为已经扩容了新的容器所以重置已使用长度m_ChunkLength 0;//把新的块重新赋值给当前存储块m_ChunkChars数组m_ChunkChars chunkChars;AssertInvariants();
}这段代码是扩容的核心操作通过这个我们可以清晰的了解到StringBuilder的存储本质•首先StringBuilder的本质是单向链表操作StringBuilder本身包含了m_ChunkPrevious指向的是上一个扩容时保存的数据。•然后StringBuilder每次扩容的长度是不固定的实际的扩容长度是max(当前追加字符的剩余长度,min(当前StringBuilder长度,8000))由此我们可以以得知一个块m_ChunkChars数组的大小最大是8000。StringBuilder还包含了一个通过StringBuilder构建实例的方法这个构造函数就是给扩容时候构建单向链表使用的它的实现也很简单private StringBuilder(StringBuilder from)
{m_ChunkLength from.m_ChunkLength;m_ChunkOffset from.m_ChunkOffset;m_ChunkChars from.m_ChunkChars;m_ChunkPrevious from.m_ChunkPrevious;m_MaxCapacity from.m_MaxCapacity;AssertInvariants();
}其目的就是把扩容之前的存储相关的各种数据传递给新的StringBuilder实例。好了到目前为止Append的核心逻辑就说完了我们大致捋一下Append的核心逻辑我们先大致罗列一下举个例子•1.默认情况m_ChunkChars[16]m_ChunkOffset0m_ChunkPreviousnullLength0•2.第一次扩容m_ChunkChars[16]m_ChunkOffset16m_ChunkPrevious指向最原始的StringBuilderm_ChunkLength16•3.第二次扩容m_ChunkChars[32]m_ChunkOffset32m_ChunkPrevious扩容之前的m_ChunkChars[16]的StringBuilderm_ChunkLength32•4.第三次扩容m_ChunkChars[64]m_ChunkOffset64m_ChunkPrevious扩容之前的m_ChunkChars[64]的StringBuilderm_ChunkLength64大概花了一张图不知道能不能辅助理解一下StringBuilder的数据结构StringBuilder的链表结构是当前节点指向上一个StringBuilder即当前扩容之前的StringBuilder的实例c# StringBuilder的数据结构本身来说是一个单向链表扩容的本质就是给这个链表新增一个节点每次扩容新增的节点存储块的容量都会增加。大部分使用时遇到的情况是首次为16、二次为16、三次为32、四次为64以此类推。转换成字符串通过上面StringBuilder的数据结构我们了解到StringBuilder本质的数据结构是单向链表这个单向链表包含m_ChunkPrevious指向上一个StringBuilder实例也就是一个倒序的链表。我们最终拿到StringBuilder的构建结果是通过StringBuilder的ToString()方法进行的得到最终的一个结果字符串接下来我们就来看一下ToString的实现[点击查看源码[8]]//当前StringBuilder实际存储的总长度
public int Length
{//StringBuilder已存储的长度块的偏移量当前块使用的长度get m_ChunkOffset m_ChunkLength;set{//注意这里是有代码的只是我们暂时省略set逻辑}
}
public override string ToString()
{AssertInvariants();//当前StringBuilder长度为0则直接返回空字符串if (Length 0){return string.Empty;}//FastAllocateString函数负责分配长度为StringBuilder长度的字符串//这个字符串就是ToString最终返回的结果,所以长度等于StringBuilder的长度string result string.FastAllocateString(Length);//当前StringBuilder是遍历的第一个链表节点StringBuilder? chunk this;do{//当前使用长度必须大于0也就是说当前块的m_ChunkChars必须使用过,才需要遍历当前节点if (chunk.m_ChunkLength 0){// 取出当前遍历的StringBuilder的相关数据// 当前遍历StringBuilder的m_ChunkCharschar[] sourceArray chunk.m_ChunkChars;int chunkOffset chunk.m_ChunkOffset;int chunkLength chunk.m_ChunkLength;// 检查是否越界if ((uint)(chunkLength chunkOffset) (uint)result.Length || (uint)chunkLength (uint)sourceArray.Length){throw new ArgumentOutOfRangeException();}//把当前遍历项StringBuilder的m_ChunkChars逐步添加到result中当前结果的前端Buffer.Memmove(ref Unsafe.Add(ref result.GetRawStringData(), chunkOffset),ref MemoryMarshal.GetArrayDataReference(sourceArray),(nuint)chunkLength);}//获取当前StringBuilder的前一个节点,循环遍历链表操作chunk chunk.m_ChunkPrevious;}//如果m_ChunkPreviousnull则代表是第一个节点while (chunk ! null);return result;
}关于这个ToString操作本质就是一个倒序链表的遍历操作每一次遍历完成都获取当前StringBuilder的上一个StringBuilder节点结束的条件就是m_ChunkPreviousnull说明该节点是首节点最终拼接成一个string字符串返回。关于这个执行的遍历过程大概可以理解为这么一个过程比如咱们的StringBuilder里存放的是我和我的祖国一刻也不能分割,无论我走到哪里都留下一首赞歌。那么针对ToString遍历StringBuilder的遍历过程则是大致如下的效果//初始化一个等于StringBuilder长度的字符串
string result \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0;
//第一次遍历后
result \0\0\0\0\0\0\0\0\0\0\0\0\0\0无论我走到哪里都留下一首赞歌。;
//第二次遍历后
result \0\0\0\0\0\0\0一刻也不能分割,无论我走到哪里都留下一首赞歌。;
//第三次遍历后
result \0\0\0我的祖国一刻也不能分割,无论我走到哪里都留下一首赞歌。;
//第三次遍历后
result 我和我的祖国一刻也不能分割,无论我走到哪里都留下一首赞歌。;毕竟StringBuilder只能记录上一个StringBuilder的数据因此这是一个倒序遍历StringBuilder链表的操作每次遍历都是向前添加m_ChunkPrevious中记录的数据直到m_ChunkPreviousnull则遍历完成直接返回结果。c# StringBuilder类的ToString本质就是倒序遍历单向链表把结果组装成一个字符串。对比java实现我们可以看到在C#上StringBuilder的实现本质是一个链表。那么和C#语言类似的Java实现思路是否一致的咱们大致看一下Java中StringBuilder的实现思路如何我本地的jdk版本为1.8.0_191首先也是初始化逻辑//存储块也就是承载Append数据的容器
char[] value;
//StringBuilder的总长度
int count;
public StringBuilder() {//默认的容量也是16super(16);
}public StringBuilder(String str) {//这个地方有差异如果通过指定字符串初始化StringBuilder//则初始化的长度则是当前传递的str的长度16super(str.length() 16);append(str);
}// AbstractStringBuilder.java
AbstractStringBuilder(int capacity) {value new char[capacity];
}在这里可以看到java的初始化容量的逻辑和c#有点不同c#默认的初始化长度取决于能存储初始化字符串的长度为主而java的实现则是在当前长度上16的长度也就是无论如何这个初始化的16的长度必须要有。那么我们再来看一下append的实现源码// AbstractStringBuilder.java
public AbstractStringBuilder append(String str) {if (str null)return appendNull();int len str.length();// 这里是扩容操作ensureCapacityInternal(count len);str.getChars(0, len, value, count);//每次append之后重新设置长度count len;return this;
}核心的是扩容ensureCapacityInternal的方法咱们简单的看下它的实现private void ensureCapacityInternal(int minimumCapacity) {//当前需要的长度char[]的长度则需要扩容if (minimumCapacity - value.length 0)expandCapacity(minimumCapacity);
}void expandCapacity(int minimumCapacity) {//新扩容的长度是当前块char[]的长度的2倍2int newCapacity value.length * 2 2;if (newCapacity - minimumCapacity 0)newCapacity minimumCapacity;if (newCapacity 0) {if (minimumCapacity 0)throw new OutOfMemoryError();newCapacity Integer.MAX_VALUE;}//把当前的char[]复制到新扩容的字符数组中value Arrays.copyOf(value, newCapacity);
}// Arrays.java copy的逻辑
public static char[] copyOf(char[] original, int newLength) {//声明一个新的数组把original的数据copy到新的char数组中char[] copy new char[newLength];System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));return copy;
}最后要展示的则是得到StringBuilder结果的操作同样是toString方法咱们看一下java中这个逻辑的实现Override
public String toString() {// 这里创建了一个新的String对象返回通过当前char[]初始化这个字符串return new String(value, 0, count);
}到了这里关于java中StringBuilder的实现逻辑相信大家都看的非常清楚了这里和c#的实现逻辑确实是不太一样本质的底层数据结构都是不一样的这里咱们简单的罗列一下它们实现方式的不同•c#中StringBuilder的数据结构是单向链表java中则是char[]字符数组。•c#中StringBuilder的初始长度是可容纳当前初始化字符串的长度java的初始化长度则是当前传递的字符串长度16。•c#中StringBuilder的扩容是生成一个新的StringBuilder实例容量和上一个StringBuilder长度有关。java则是生成一个是原来char[]数组长度*22长度的新数组。•c#中ToString的实现是遍历倒序链表组装一个新的字符串返回java上则是用当前StringBuilder的char[]初始化一个新的字符串返回。关于c#和java的StringBuilder实现方式差异如此之大到底哪种实现方式更优一点呢这个没办法评价毕竟每一门语言的底层类库实现都是经过深思熟虑的集成了很多人的思想。在楼主的角度来看StringBuilder本身的核心功能在于构建的过程所以构建过程的性能非常重要所以类似数组扩容再copy的逻辑没有链表的方式高效。但是在最后的ToString得到结果的时候数组的优势是非常明显的毕竟string本质就是一个char[]数组。对于StringBuilder来说append是频繁操作大部分情况可能多次进行append操作而ToString操作对于StringBuilder来说基本上只有一次那就是得到StringBuilder构建结果的时候。所以楼主觉得提升append的性能是关键。总结 本文我们主要讲解了c# StringBuilder的大致的实现方式同时也对比了c#和java关于实现方式的StringBuilder的不同主要差异是c#实现的底层数据结构为单向链表java实现的方式则是数组。这也为我们提供了不同的思路在这里我们也再次总结一下它的实现方式•c# StringBuilder的本质是单向链表操作StringBuilder本身包含了m_ChunkPrevious指向的是上一个扩容时保存的数据,扩容的本质就是给这个链表新增一个节点。•c# StringBuilder每次扩容的长度是不固定的实际的扩容长度是max(当前追加字符的剩余长度,min(当前StringBuilder长度,8000))每次扩容新增的节点存储块的容量都会增加。大部分使用时遇到的情况是首次为16、二次为16、三次为32、四次为64以此类推。•c# StringBuilder类的ToString本质就是倒序遍历单向链表把结果组装成一个字符串。•关于c#和java实现StringBuilder存在很大差异主要差异是c#实现的底层数据结构为单向链表java实现的方式则是数组。虽然大家都说越努力越幸运,有时候我们努力是为了让自己更幸运。但是我更喜欢的是我们努力不仅仅是为了幸运而是让我们的心里更踏实结果固然重要然而许多时候努力过了也就问心无愧了。References[1] 点击查看源码: https://github.com/dotnet/runtime/blob/v6.0.2/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs#L77[2] 点击查看源码: https://github.com/dotnet/runtime/blob/v6.0.2/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs#L160[3] 点击查看源码: https://github.com/dotnet/runtime/blob/v6.0.2/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs#L118[4] 点击查看源码: https://github.com/dotnet/runtime/blob/v6.0.2/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs#L793[5] 点击查看源码: https://github.com/dotnet/runtime/blob/v6.0.2/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs#L836[6] 点击查看源码: https://github.com/dotnet/runtime/blob/v6.0.2/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs#L2095[7] 点击查看源码: https://github.com/dotnet/runtime/blob/v6.0.2/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs#L2372[8] 点击查看源码: https://github.com/dotnet/runtime/blob/v6.0.2/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs#L346