在前面的博文《小学徒成长系列—String关键源码解析》和《小学徒进阶系列—JVM对String的处理》中,我们讲到了关于String的常用方法以及JVM对字符串常量String的处理。

但是在Java中,关于字符串操作的类还有两个,它们分别是StringBuilder和StringBuffer。我们先来就讲解一下String类和StringBuilder、StringBuffer的联系吧。

String、StringBuilder、StringBuffer的异同点

结合之前写的博文,我们对这三个常用的类的异同点进行分析:

异:

1>String的对象是不可变的;而StringBuilder和StringBuffer是可变的。

2>StringBuilder不是线程安全的;而StringBuffer是线程安全的

3>String中的offset,value,count都是被final修饰的不可修改的;而StringBuffer和StringBuilder中的value,count都是继承自AbstractStringBuilder类的,没有被final修饰,说明他们在运行期间是可修改的,而且没有offset变量。

同:

三个类都是被final修饰的,是不可被继承的。

StringBuilder和StringBuffer的构造方法

其实StringBuilder和StringBuffer的构造方法类型是一样的,里面都是通过调用父类的构造方法进行实现的,在这里,我主要以StringBuilder为例子讲解,StringBuffer就不重复累赘的讲啦。

1>构建一个初始容量为16的默认的字符串构建

 public StringBuilder() {
super(16);
}

从构造方法中我们看到,构造方法中调用的是父类AbstractStringBuilder中的构造方法,我们来看看,父类中的构造方法:

 /**
* 构造一个不带任何字符的字符串生成器,其初始容量由 capacity 参数指定。
* @params capacity 数组初始化容量
*/
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}

这个构造方法说明的是,创建一个初始容量由 capacity 参数指定的字符数组,而子类中传过来的是16,所以创建的就是初始容量为16的字符数组

2>构造一个不带任何字符的字符串生成器,其初始容量由 capacity 参数指定。

 public StringBuilder(int capacity) {
super(capacity);
}

这个构造方法调用的跟上面1>的构造方法是同一个的,只是这里子类中的初始化容量由用户决定。

3>构造一个字符串生成器,并初始化为指定的字符串内容。该字符串生成器的初始容量为 16 加上字符串参数的长度。

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

这个构造方法首先调用和1>一样的父类构造方法,然后再调用本类中的append()方法将字符串str拼接到本对象已有的字符串之后。

4>构造一个字符串生成器,包含与指定的 CharSequence 相同的字符。该字符串生成器的初始容量为 16 加上 CharSequence 参数的长度。

 public StringBuilder(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}

嗯,这个构造方法,大家一看就知道跟上面的差不多啦,我就不介绍啦。

StringBuilder常用的方法

在StringBuilder中,很多方法最终都是进行一定的逻辑处理,然后通过调用父类AbstractStringBuilder中的方法进行实现的。

1>append(String str)

从下面的代码中我们可以看到,他是直接调用父类的append方法进行实现的。

 public StringBuilder append(String str) {
super.append(str);
return this;
}

下面我们再看下父类AbstractStringBuilder中的append方法是怎么写的

 public AbstractStringBuilder append(String str) {
//注意,当str的值为nul时,将会在当前字符串对象后面添加上Null字符串
if (str == null) str = "null";
//获取需要添加的字符串的长度
int len = str.length();
//判断添加后的字符串对象是否超过容量,若是,扩容
ensureCapacityInternal(count + len);
//将str中的字符串复制到value数组中
str.getChars(0, len, value, count);
//更新当前字符串对象的字符串长度
count += len;
return this;
}

2> ensureCapacityInternal

下面我们看下,他每次拼接字符串的时候,是怎样进行扩容的:

  /**
* This method has the same contract as ensureCapacity, but is
* never synchronized.
*/
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
// 如果需要扩展到的容量比当前字符数组长度要大
// 那么就正常扩容
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
} /**
* This implements the expansion semantics of ensureCapacity with no
* size check or synchronization.
*/
void expandCapacity(int minimumCapacity) {
// 初始化新的容量大小为当前字符串长度的2倍加2
int newCapacity = value.length * 2 + 2;
// 如果新容量大小比传进来的最小容量还要小
// 就是用最小的容量为新数组的容量
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
// 如果新的容量或者最小容量小于0
// 抛异常并且讲新容量设置成Integer最能存储的最大值
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
// 创建容量大小为newCapacity的新数组
value = Arrays.copyOf(value, newCapacity);
}

3>append(StringBuffer sb)

从这里我们可以看到,它又是调用父类的方法进行拼接的。

 public StringBuilder append(StringBuffer sb) {
super.append(sb);
return this;
}

继续看父类中的拼接方法:

  // Documentation in subclasses because of synchro difference
public AbstractStringBuilder append(StringBuffer sb) {
// 如果sb的值为null,这里就会为字符串添加上字符串“null”
if (sb == null)
return append("null");
// 获取需要拼接过来的字符串的长度
int len = sb.length();
// 扩容当前兑现搞定字符数组容量
ensureCapacityInternal(count + len);
// 进行字符串的拼接
sb.getChars(0, len, value, count);
// 更新当前字符串对象的长度变量
count += len;
return this;
}

4>public StringBuilder delete(int start, int end)

删除从start开始到end结束的字符(包括start但不包括end)

 public StringBuilder delete(int start, int end) {
super.delete(start, end);
return this;
}

是的,又是调用父类进行操作的。

 /**
* Removes the characters in a substring of this sequence.
* The substring begins at the specified {@code start} and extends to
* the character at index {@code end - 1} or to the end of the
* sequence if no such character exists. If
* {@code start} is equal to {@code end}, no changes are made.
*
* @param start The beginning index, inclusive.
* @param end The ending index, exclusive.
* @return This object.
* @throws StringIndexOutOfBoundsException if {@code start}
* is negative, greater than {@code length()}, or
* greater than {@code end}.
*/
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;
}

其实看了那么多,我们也很容易发现,不管是String类还是现在博文中的StringBuilder和StringBuffer,底层实现都用到了Arrays.copyOfRange(original, from, to);和System.arraycopy(src, srcPos, dest, destPos, length);这两个方法实现的。

在看完上面那段源代码之后,我突然想到了一个问题,就是如果需要剩下的字符个数少于需要被覆盖的字符个数时怎么办,看下面的代码:

 import java.util.Arrays;

 public class StringBuilderTest {
public static void main(String[] args) {
char[] src = {'a', 'b', 'c', 'd', 'e', 'f', 'g'};
int start = 4;
int end = 5;
int len = end - start;
if (len > 0) {
//进行复制,将被删除
System.arraycopy(src, start+len, src, start, src.length-end);
}
System.out.println(src); StringBuilder stringBuilder = new StringBuilder("abcdefg");
stringBuilder.delete(4, 5);
System.out.println(stringBuilder);
}
}

结果输出了:

aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEcAAAAhCAIAAAAEfyEPAAABT0lEQVRYhe2X3ZGFIAyFqSsFpZ5Uk2YohvvA3fyoOMLiGHc5bwYIfOTgTFL5i0pPH+AWLar36J9RMaaUgPJY2u/qXegr5LG0V3VWK4JhqlIKo6XKBB7yVqV6ALlFy0EAxAQHI2YFkMJnmZyQlcpWKbkayggQWW+04n1ULI4gUHsQmIRZRxjVQ5ngZw6j7r5z4FGtNKPJchLvpDJ3bE1vCess5LK1lojRbb/B2FP5SJaKt+KdVPaO5eiVanvO+6h0eSveR2WzZAJfq+SG1IFmJ/Ejoykt4xUHahoC68DjeA+VMyAgQjVhfRn1c/+3sJb1KBJFeVrO33aB3RjAGeYw3kUVQa33M/yunpQpCl2JX1SUWs3VonqPFtV7tKjeo9U19ml1jaPd4TlVrK5xElWornGG4nWNMxSxa5xBFbBrnEIVQdPf1ZP6ZXfYUpRazdUHNS9pAURJ7aMAAAAASUVORK5CYII=" alt="" />

奇怪,为什么StringBuilder可以输出abcdefg而我的会多了一个g呢?原因是在StringBuilder中的toString方法中重新创建了一个有效数字为count的,也就是说值为abcdefg的字符串对象,如下代码:

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

5>public StringBuilder replace(int start, int end, String str)

关于这个方法,因为是直接调用父类中的方法进行实现的,所以我们继续直接看父类中的方法吧:

 /**
* Replaces the characters in a substring of this sequence
* with characters in the specified <code>String</code>. The substring
* begins at the specified <code>start</code> and extends to the character
* at index <code>end - 1</code> or to the end of the
* sequence if no such character exists. First the
* characters in the substring are removed and then the specified
* <code>String</code> is inserted at <code>start</code>. (This
* sequence will be lengthened to accommodate the
* specified String if necessary.)
*
* @param start The beginning index, inclusive.
* @param end The ending index, exclusive.
* @param str String that will replace previous contents.
* @return This object.
* @throws StringIndexOutOfBoundsException if <code>start</code>
* is negative, greater than <code>length()</code>, or
* greater than <code>end</code>.
*/
public AbstractStringBuilder replace(int start, int end, String str) {
// 健壮性的检查
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (start > count)
throw new StringIndexOutOfBoundsException("start > length()");
if (start > end)
throw new StringIndexOutOfBoundsException("start > end"); if (end > count)
end = count; // 获取需要添加的字符串的长度
int len = str.length();
// 计算新字符串的长度
int newCount = count + len - (end - start);
// 对当前对象的数组容量进行扩容
ensureCapacityInternal(newCount);
// 进行数组的中的元素移位,从而空出足够的空间来容纳需要添加的字符串
System.arraycopy(value, end, value, start + len, count - end);
// 将str复制到value中
str.getChars(value, start);
// 更新字符串长度
count = newCount;
return this
}

6>public StringBuilder insert(int offset, String str)

在offset位置插入字符串str,他的实现也是通过父类进行实现的,继续看父类中的相应方法:

 /**
* Inserts the string into this character sequence.
* <p>
* The characters of the {@code String} argument are inserted, in
* order, into this sequence at the indicated offset, moving up any
* characters originally above that position and increasing the length
* of this sequence by the length of the argument. If
* {@code str} is {@code null}, then the four characters
* {@code "null"} are inserted into this sequence.
* <p>
* The character at index <i>k</i> in the new character sequence is
* equal to:
* <ul>
* <li>the character at index <i>k</i> in the old character sequence, if
* <i>k</i> is less than {@code offset}
* <li>the character at index <i>k</i>{@code -offset} in the
* argument {@code str}, if <i>k</i> is not less than
* {@code offset} but is less than {@code offset+str.length()}
* <li>the character at index <i>k</i>{@code -str.length()} in the
* old character sequence, if <i>k</i> is not less than
* {@code offset+str.length()}
* </ul><p>
* The {@code offset} argument must be greater than or equal to
* {@code 0}, and less than or equal to the {@linkplain #length() length}
* of this sequence.
*
* @param offset the offset.
* @param str a string.
* @return a reference to this object.
* @throws StringIndexOutOfBoundsException if the offset is invalid.
*/
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复制到value数组中
str.getChars(value, offset);
// 更新当前对象中记录的长度
count += len;
return this;
}

7>indexOf(String str)

其实这个的实现主要是借助了String对象的indexOf方法来实现的,具体可以参考博文:http://www.cnblogs.com/xiaoxuetu/archive/2013/06/05/3118229.html 这里就不详细进行讲解了:

 /**
* @throws NullPointerException {@inheritDoc}
*/
public int indexOf(String str) {
return indexOf(str, 0);
}

调用了同一个类中的indexOf方法:

 /**
* @throws NullPointerException {@inheritDoc}
*/
public int indexOf(String str, int fromIndex) {
//调用了String类中的静态方法indexOf
return String.indexOf(value, 0, count,
str.toCharArray(), 0, str.length(), fromIndex);
}

String.indexOf()方法是默认权限的,也就是只有与他同包的情况下才能够进行访问这个方法。

8> lastIndexOf()

  lastIndexOf()方法跟indexOf()差不多,调用了String.lastIndexOf()方法进行实现,再次不重复说明。

9> public StringBuilder reverse()

我们经常进行字符串的逆转,面试的时候也有经常问到,那么实际上在jdk中式怎么完成这些操作的呢?

首先我们看下StringBuilder中的reverse方法():

 public StringBuilder reverse() {
//调用父类的reverse方法
super.reverse();
return this;
}

一般情况下,如果让我们来进行逆转,会怎么写呢?我想很多人都会像下面那样子写吧:

 public String reverse(char[] value){
//折半,从中间开始置换
for (int i = (value.length - 1) >> 1; i >= 0; i--){
char temp = value[i];
value[i] = value[value.length - 1 - i];
value[value.length - 1 - i] = temp;
}
return new String(value);
}

确实很简单,但是一个完整的 Unicode 字符叫代码点CodePoint,而一个 Java char 叫 代码单元 code unit。如果String 对象以UTF-16保存 Unicode 字符,需要用2个字符表示一个超大字符集的汉字,这这种表示方式称之为 Surrogate,第一个字符叫 Surrogate High,第二个就是 Surrogate Low。所在在JDK中也加入了判断一个char是否是Surrogate区的字符:

 /**
* Causes this character sequence to be replaced by the reverse of
* the sequence. If there are any surrogate pairs included in the
* sequence, these are treated as single characters for the
* reverse operation. Thus, the order of the high-low surrogates
* is never reversed.
*
* Let <i>n</i> be the character length of this character sequence
* (not the length in <code>char</code> values) just prior to
* execution of the <code>reverse</code> method. Then the
* character at index <i>k</i> in the new character sequence is
* equal to the character at index <i>n-k-1</i> in the old
* character sequence.
*
* <p>Note that the reverse operation may result in producing
* surrogate pairs that were unpaired low-surrogates and
* high-surrogates before the operation. For example, reversing
* "\uDC00\uD800" produces "\uD800\uDC00" which is
* a valid surrogate pair.
*
* @return a reference to this object.
*/
public AbstractStringBuilder reverse() {
// 默认没有存储到Surrogate区的字符
boolean hasSurrogate = false;
int n = count - 1;
// 折半,遍历并且首尾相应位置置换
for (int j = (n-1) >> 1; j >= 0; --j) {
char temp = value[j];
char temp2 = value[n - j];
if (!hasSurrogate) {
// 判断一个char是否是Surrogate区的字符
hasSurrogate = (temp >= Character.MIN_SURROGATE && temp <= Character.MAX_SURROGATE)
|| (temp2 >= Character.MIN_SURROGATE && temp2 <= Character.MAX_SURROGATE);
}
// 首尾值置换
value[j] = temp2;
value[n - j] = temp;
} // 如果含有Surrogate区的字符
if (hasSurrogate) {
// Reverse back all valid surrogate pairs
for (int i = 0; i < count - 1; i++) {
char c2 = value[i];
if (Character.isLowSurrogate(c2)) {
char c1 = value[i + 1];
if (Character.isHighSurrogate(c1)) {
//下面这行代码相当于
//value[i]=c1; i=i+1;
value[i++] = c1;
value[i] = c2;
}
}
}
}
return this;
}

StringBuffer的常用方法

前面我们知道StringBuffer相当于StringBuilder来说是线程安全的,所以再StringBuffer中,所有的方法都加了同步synchronized,例如append(String str)方法:

public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}

具体内部实现就不详细说明啦,跟StringBuilder是一样的,大部分都是调用AbstractStringBuilder进行实现的。

小学徒成长系列—StringBuilder & StringBuffer关键源码解析的更多相关文章

  1. 老生常谈系列之Aop--Spring Aop源码解析(一)

    老生常谈系列之Aop--Spring Aop源码解析(一) 前言 上一篇文章老生常谈系列之Aop--Spring Aop原理浅析大概阐述了动态代理的相关知识,并且最后的图给了一个Spring Aop实 ...

  2. DotNetOpenAuth Part 1 : Authorization 验证服务实现及关键源码解析

    DotNetOpenAuth 是 .Net 环境下OAuth 开源实现框架.基于此,可以方便的实现 OAuth 验证(Authorization)服务.资源(Resource)服务.针对 DotNet ...

  3. 老生常谈系列之Aop--Spring Aop源码解析(二)

    老生常谈系列之Aop--Spring Aop源码解析(二) 前言 上一篇文章老生常谈系列之Aop--Spring Aop源码解析(一)已经介绍完Spring Aop获取advice切面增强方法的逻辑, ...

  4. ThreadPoolExecutor系列<三、ThreadPoolExecutor 源码解析>

    本文系作者原创,转载请注明出处:http://www.cnblogs.com/further-further-further/p/7681826.html 在源码解析前,需要先理清线程池控制的运行状态 ...

  5. Java 集合系列Stack详细介绍(源码解析)和使用示例

    Stack简介 Stack是栈.它的特性是:先进后出(FILO, First In Last Out). java工具包中的Stack是继承于Vector(矢量队列)的,由于Vector是通过数组实现 ...

  6. 【OpenStack】OpenStack系列13之Nova源码解析与API扩展

    学习思路 议程:代码结构-主干流程-分层架构-业务模型-数据库模型-消息模型 分布式架构:Api:横向扩展    rpc:纵向扩展 分层架构:Controller接口层.View/Manager逻辑层 ...

  7. TiKV 源码解析系列 - Raft 的优化

    本系列文章主要面向 TiKV 社区开发者,重点介绍 TiKV 的系统架构,源码结构,流程解析.目的是使得开发者阅读之后,能对 TiKV 项目有一个初步了解,更好的参与进入 TiKV 的开发中.本文是本 ...

  8. Maven 依赖调解源码解析(三):传递依赖,路径最近者优先

    本文是系列文章<Maven 源码解析:依赖调解是如何实现的?>第三篇,主要介绍依赖调解的第一条原则:传递依赖,路径最近者优先.本篇内容较多,也是开始源码分析的第一篇,请务必仔细阅读,否则后 ...

  9. Maven 依赖调解源码解析(四):传递依赖,第一声明者优先

    本文是系列文章<Maven 源码解析:依赖调解是如何实现的?>第四篇,主要介绍依赖调解的第二条原则:传递依赖,第一声明者优先.请按顺序阅读其他系列文章,系列文章总目录参见:https:// ...

随机推荐

  1. winform快速开发平台 -> 快速绑定ComboBox数据控件

    通常我们在处理编辑窗体时.往往会遇到数据绑定.例如combobox控件绑定数据字典可能是我们经常用到的.然而在我的winform快速开发平台中我是如何处理这个频繁的操作呢? 首先,我们要绑定combo ...

  2. [leetcode] 题型整理之图论

    图论的常见题目有两类,一类是求两点间最短距离,另一类是拓扑排序,两种写起来都很烦. 求最短路径: 127. Word Ladder Given two words (beginWord and end ...

  3. iOS drewRect方法

    You do not need to override this method if your view sets its content in other ways. By the time thi ...

  4. TFS 分支导致nuget项目依赖丢失

    问题: 项目的代码 在tfs上分支后,签出项目.编译时发现无法编译,原有的nuget来的包的dll都丢失了(项目签入时,默认会忽略dll) 在网上找了下,发现一个简单的解决方法: 在"程序包 ...

  5. bind模拟

    if (!Function.prototype.bind) { Function.prototype.bind = function(oThis) { if (typeof this !== 'fun ...

  6. Eclipse 执行成功的 Hadoop-1.2.1 WordCount 源码

    万事开头难.最近在学习Hadoop,先是搭建各种版本环境,从2.2.0到2.3.0,再到1.2.1,终于都搭起来了,折腾了1周时间,之后开始尝试使用Eclipse编写小demo.仅复制一个现成的Wor ...

  7. 工厂模式(Factory)

    一.分类 工厂模式主要是为创建对象提供过渡接口,以便将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的. 工厂模式主要分为三个,简单工厂模式(Simple Factory)/ 工厂方法模式(Fac ...

  8. IO总结

    在电脑是新建一个文件夹 File file = new File("F:\\imgs"); File file = new File("F:/imgs"); 输 ...

  9. C#在Linux+Mono环境中使用微信支付证书

    最近特殊的需求,要把微信平台一个功能页面部署到Linux(CentOS6.5)下,其中涉及到微信支付退款. 鉴于之前实践过mono+jexus+asp.net mvc的部署,于是问题重点在于解决对商户 ...

  10. 支持“ApplicationDbContext”上下文的模型已在数据库创建后发生更改

    异常信息 解决方法: 1.PM> Enable-Migrations 2.打开生成的Configuration.cs文件,修改代码如下 public Configuration() { Auto ...