Java StringBuffer StringBuilder类源码解析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: StringBuffer是线程安全的字符动态序列,像String但是可以修改,在任何时点他都含有字符的特定序列,但是序列的长度和内容可以通过调用某些方法来修改。 StringBuffer对于多线程是安全的,在必要的方法上都加了synchronized。

StringBuffer

StringBuffer是线程安全的字符动态序列,像String但是可以修改,在任何时点他都含有字符的特定序列,但是序列的长度和内容可以通过调用某些方法来修改。

StringBuffer对于多线程是安全的,在必要的方法上都加了synchronized。核心方法是append和insert,他们通过重载可以接受任何类型的数据。将数据转换为String然后扩展或者插入到StringBuffer中。append将字符添加到末尾,insert是添加到某个指定的位置。举个例子,z是一个StringBuffer,当前内容为"start",此时调用z.append("le")则内容变为"startle",若调用的是z.insert(4, "le")则内容变为"starlet"。sb是一个StringBuffer,sb.append(x)和sb.insert(sb.length(), x)是等效的。

当有一个包含源序列的操作发生时,只有StringBuffer同步操作,不会发生在源上。

由于StringBuffer被设计为线程安全类,所以在通过一个被多个线程共享的源序列构造和append insert操作时,调用的程序必须确保在这些操作期间源序列没有发生变化。这个可以通过调用者在操作期间加锁来保证,或者通过使用一个不可变的源序列,或者不使用线程共享的源序列。

除非另外说明,对构建或者其他方法传入一个null参数会引起抛出NullPointerException错误。

JDK5中,补充了StringBuffer的单线程版本StringBuilder,StringBuilder应该优先使用,他有同样的操作但是没有synchronized所以速度更快。

内部变量与构造函数

从类的定义中可以看出StringBuffer继承了AbstractStringBuilder,下面会介绍到复用了AbstractStringBuilder的内部变量与函数

 public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence

StringBuffer自身有一个内部变量toStringCache,这是上一个toString返回值的高速缓存,一旦StringBuffer被修改就会清空,作用是在调用toString的时如果没有变更可以快速返回结果不用重新构造字符串

    private transient char[] toStringCache;

观察StringBuffer的构造函数,可以看到他们都是基于super(capacity)这个方法来展开的,也就是AbstractStringBuilder的构造函数

    //构造一个初始大小为16的StringBuffer
    public StringBuffer() {
        super(16);
    }
    //构造指定初始容量大小
    public StringBuffer(int capacity) {
        super(capacity);
    }
    //构造一个StringBuffer,初始内容为str,初始大小为16+str的长度
    public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    }
    //构造一个StringBuffer内容和CharSequence一致,初始容量为16+CharSequence.length,如果CharSequence的长度为0,则返回一个空的buffer容量为16
    public StringBuffer(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }

下面的内部变量和构造函数来自AbstractStringBuilder,可以看到他的构造方法主要是新分配了一个给定大小的数组

    char[] value;//存储字符

    int count;//字符个数

    AbstractStringBuilder(int capacity) {
        value = new char[capacity];//分配一个大小为capacity的字符数组给value
    }

下面两个方法是对容量和字符长度的查询,只做查询而不会做出修改

    public synchronized int length() {
        return count;//返回字符个数
    }

    public synchronized int capacity() {
        return value.length;//返回容量大小也就是数组大小
    }

而ensureCapacity是会修改数组大小的,他会确保value数组的大小不小于minimumCapacity,如果容量小于该大小,会分配一个新的数组并将原本的字符复制到新数组中,新数组大小是当前容量*2+2和minimumCapacity中的较大值,minimumCapacity有大小限制,超过一定的值会内存溢出

    public synchronized void ensureCapacity(int minimumCapacity) {
        super.ensureCapacity(minimumCapacity);//确保value数组的大小不小于minimumCapacity
    }
//下面的代码来自父类
    //确保容量不小于最小值,如果当前容量小于参数值,分配一个新的更大的内部数组,他的大小是minimumCapacity和旧容量旧容量*2+2中的较大值。如果minimumCapacity是负数,什么也不做直接返回。
    public void ensureCapacity(int minimumCapacity) {
        if (minimumCapacity > 0)
            ensureCapacityInternal(minimumCapacity);
    }

    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));//根据minimumCapacity分配一个新的数组,并将原来的字符复制到新的数组中
        }
    }
    //返回不小于minCapacity的大小,如果当前大小*2+2足够的话就取该值。不会返回超过MAX_ARRAY_SIZE的大小,除非minCapacity超过该值
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }//新的大小为旧大小*2+2与minCapacity中的较大值
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }
    //如果minCapacity在MAX_ARRAY_SIZE到Integer.MAX_VALUE之间的话返回minCapacity,超过Integer.MAX_VALUE抛出OutOfMemoryError
    private int hugeCapacity(int minCapacity) {
        if (Integer.MAX_VALUE - minCapacity < 0) { // 内存溢出
            throw new OutOfMemoryError();
        }
        return (minCapacity > MAX_ARRAY_SIZE)
            ? minCapacity : MAX_ARRAY_SIZE;
    }

trimToSize在value中存在没有存储的空间时,会重新分配一个大小和字符个数相等的数组将字符复制过去,提高空间利用率,会改变capacity()的值

    public synchronized void trimToSize() {
        super.trimToSize();//新分配一个数组仅保留与字符个数相等的大小,将字符复制过去
    }
    //尝试减少用于存储字符串的空间。如果缓冲区比保存当前字符串所需的空间更大,会变更大小提高空间利用率。这个方法可能会改变capacity()的返回值
    public void trimToSize() {
        if (count < value.length) {
            value = Arrays.copyOf(value, count);//新分配一个大小为字符个数的数组,将现有的字符复制过去
        }
    }

setLength在newLength小于等于当前数组大小时直接返回,大于时新分配一个大小为newLength和当前容量*2+2的较大值的新数组,并复制字符,然后将数组中的剩余位置填充上'0',count设为newLength

    public synchronized void setLength(int newLength) {
        toStringCache = null;//清空上一次toString的缓存
        super.setLength(newLength);
    }

    public void setLength(int newLength) {
        if (newLength < 0)
            throw new StringIndexOutOfBoundsException(newLength);
        ensureCapacityInternal(newLength);//newLength小于等于当前数组大小的话直接返回,否则分配一个大小为newLength和当前容量*2+2的较大值的新数组,并复制字符

        if (count < newLength) {
            Arrays.fill(value, count, newLength, '\0');//字符个数小于newLength时,用'\0'填充剩余的位置
        }

        count = newLength;//count设为newLength
    }

charAt返回指定位置的字符,会检查index返回是否大于等于0且小于count

    public synchronized char charAt(int index) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        return value[index];
    }   

    public char charAt(int index) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        return value[index];
    }

codePointAt是返回index位置的代码点,代码点这个东西之前在String里讲过,这里再贴一次:字符数据类型是一个采用UTF-16编码表示Unicode代码点的代码单元。大多数的常用Unicode字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。而length返回的是UTF-16下的代码单元的数量,而codePointCount返回的是代码点的数量。对于大部分人工输入的字符,这两者是相等的,会出现length比codePointCount长的通常是某些数学或者机器符号,需要两个代码单元来表示一个代码点 。codePointBefore返回index前一个位置的代码点,codePointCount则是统计指定序列段中的代码点数量

    public synchronized int codePointAt(int index) {
        return super.codePointAt(index);
    }

    public int codePointAt(int index) {
        if ((index < 0) || (index >= count)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return Character.codePointAtImpl(value, index, count);
    }

    public synchronized int codePointBefore(int index) {
        return super.codePointBefore(index);
    }

    public int codePointBefore(int index) {
        int i = index - 1;
        if ((i < 0) || (i >= count)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return Character.codePointBeforeImpl(value, index, 0);
    }

    public synchronized int codePointCount(int beginIndex, int endIndex) {
        return super.codePointCount(beginIndex, endIndex);//统计从beginIndex到endIndex之间的代码点数量
    }

    public int codePointCount(int beginIndex, int endIndex) {
        if (beginIndex < 0 || endIndex > count || beginIndex > endIndex) {
            throw new IndexOutOfBoundsException();
        }
        return Character.codePointCountImpl(value, beginIndex, endIndex-beginIndex);
    }

offsetByCodePoints这个方法单看注释翻译比较难理解:返回从index到codePointOffset的代码点偏移index,每个不成对的代理(两个代码单元表示一个代码点时称为两个代理)在范围内被记为一个代码点。实际上可以理解为,如果不存在两个代码单元表示一个代码点的情况,返回的结果就是index+codePointOffset;如果存在那种特殊代码点,则index的变化量会偏移特殊代码点的个数,例如有3个特殊代码点,则返回值为index+codePointOffset+3(codePointOffset>0)或者index+codePointOffset-3(codePointOffset<0)

    public synchronized int offsetByCodePoints(int index, int codePointOffset) {
        return super.offsetByCodePoints(index, codePointOffset);
    }

    public int offsetByCodePoints(int index, int codePointOffset) {
        if (index < 0 || index > count) {
            throw new IndexOutOfBoundsException();
        }
        return Character.offsetByCodePointsImpl(value, 0, count,
                                                index, codePointOffset);
    }

getChars会再检查参数范围后,复制指定位置的字符串到指定的位置

    public synchronized void getChars(int srcBegin, int srcEnd, char[] dst,
                                      int dstBegin)
    {
        super.getChars(srcBegin, srcEnd, dst, dstBegin);//复制value从srcBegin到srcEnd的内容到dst从dstBegin开始的位置
    }

setCharAt修改指定位置的字符

    public synchronized void setCharAt(int index, char ch) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        toStringCache = null;//清空toString缓存
        value[index] = ch;//修改对应位置的字符
    }

核心函数之一的append有众多的重载,篇幅原因就不全贴了。append需要注意一点,直接在参数里输入null是会报错的,但是以对象赋值null的方式传入是可行的,相当于添加"null"。对于传入的非字符串对象,统一调用toString方法转换为字符串;数值对象的话通过包装类的方法转为字符串。

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

    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);//确保数组容量足够大
        str.getChars(0, len, value, count);//将str从头到尾复制到value中从count开始的位置,实现拼接
        count += len;//增加字符数量
        return this;
    }

    private AbstractStringBuilder appendNull() {
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        value[c++] = 'n';//null当做"null"来进行扩展
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }

delete删除包括start在内到end之前的字符,end开始部分保留,通过复制保留部分到start的位置来实现

    public synchronized StringBuffer delete(int start, int end) {
        toStringCache = null;//清除toString缓存
        super.delete(start, end);//删除从start到end-1位置的元素
        return this;
    }

    public AbstractStringBuilder delete(int start, int end) {
        if (start < 0)
            throw new StringIndexOutOfBoundsException(start);
        if (end > count)
            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);//将start+len开始的长度为count-end的部分,复制到start开始的位置
            count -= len;//修改count值
        }
        return this;
    }

deleteCharAt只删除单个字符,也是通过复制来实现

    public synchronized StringBuffer deleteCharAt(int index) {
        toStringCache = null;
        super.deleteCharAt(index);//将index后一位开始的内容复制到index的位置
        return this;
    }

replace操作会移除start到end-1的内容,将str插入到start开始的位置,实现的话会先把value中的后面那段复制到他最终所处的位置,中间留出一段空间供str复制进去

    public synchronized StringBuffer replace(int start, int end, String str) {
        toStringCache = null;
        super.replace(start, end, str);//移除start到end-1的内容,将str插入到start开始的位置
        return this;
    }

substring和subSequence方法截取子串,substring可以不输入end参数截取到末尾,方法都是基于父类的同一个函数来返回一个新的String

    public String substring(int start, int end) {
        if (start < 0)
            throw new StringIndexOutOfBoundsException(start);
        if (end > count)
            throw new StringIndexOutOfBoundsException(end);
        if (start > end)
            throw new StringIndexOutOfBoundsException(end - start);
        return new String(value, start, end - start);
    }

insert方法同样是重载众多,但是主要参数只有在value中插入的位置、插入的对象、插入对象从哪里开始截取、截取长度是多少,后两个可以不输入那么就是整个对象进行插入。会清空toStringCache

    public synchronized StringBuffer insert(int index, char[] str, int offset,
                                            int len)
    {
        toStringCache = null;
        super.insert(index, str, offset, len);
        return this;
    }

    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);//确保空间足够,不足时扩展为当前容量*2+2和count+len的较大值
        System.arraycopy(value, index, value, index + len, count - index);//将index开始的内容复制到index+len的位置,空出留给str的空间
        System.arraycopy(str, offset, value, index, len);//str复制到留出的空间中
        count += len;//count增加str的长度
        return this;
    }

indexOf和lastIndexOf两个方法分别是从头开始向后寻找第一个完全相等的字符串和从尾部开始从头寻找第一个,可以指定开始寻找的位置,直接调用了String的同名方法

    public synchronized int indexOf(String str, int fromIndex) {
        return super.indexOf(str, fromIndex);//调用了String.indexOf
    }

    public synchronized int lastIndexOf(String str, int fromIndex) {
        return super.lastIndexOf(str, fromIndex);//调用了String.lastIndexOf
    }

reverse这个方法会逆序字符串内容,从中心开始做轴对称的交换

    public synchronized StringBuffer reverse() {
        toStringCache = null;
        super.reverse();//以中心为轴,从中间点开始做轴对称位置的字符复制交换
        return this;
    }

toString有缓存直接返回,否则新建一个数组复制value里的有效字符。所有会导致value中内容变化的方法都会清空缓存,还有setLength无论是否导致长度变化并填充了'0'都会清空

    public synchronized String toString() {
        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);//缓存无效时,创建一个新的数组将value中的有效字符复制进去
        }
        return new String(toStringCache, true);//缓存有效时直接返回,缓存中的字符串是被共享的
    }

StringBuilder

JDK1.5加入,同样继承了AbstractStringBuilder,实现了java.io.Serializable, CharSequence接口。

StringBuilder是没有toStringCache的,所以他的toString函数必定是复制产生一个新的String,猜测是出于StringBuilder默认是用于单线程环境,不需要进行共享操作,所以也就没有了cache

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

StringBuilder在单线程情况下由于没有了同步锁性能更好,推荐优先使用。他的实现和StringBuffer除了上面提到的cache和同步的问题外几乎没有区别,另外一个有区别的地方是序列化部分。

先看StringBuilder的序列化函数,非常简单,除了缺省对象外只有count和value的读写

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        s.defaultWriteObject();
        s.writeInt(count);
        s.writeObject(value);
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        count = s.readInt();
        value = (char[]) s.readObject();
    }

而StringBuffer就不同了,用了ObjectStreamField来声明序列化的字段,至于这两个序列化的方式到底有什么区别,以后能更新到IO流的时候再说吧

    private static final java.io.ObjectStreamField[] serialPersistentFields =
    {
        new java.io.ObjectStreamField("value", char[].class),
        new java.io.ObjectStreamField("count", Integer.TYPE),
        new java.io.ObjectStreamField("shared", Boolean.TYPE),
    };

    private synchronized void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        java.io.ObjectOutputStream.PutField fields = s.putFields();
        fields.put("value", value);
        fields.put("count", count);
        fields.put("shared", false);
        s.writeFields();
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        java.io.ObjectInputStream.GetField fields = s.readFields();
        value = (char[])fields.get("value", null);
        count = fields.get("count", 0);
    }
相关文章
|
22天前
|
Java 编译器
Java 泛型详细解析
本文将带你详细解析 Java 泛型,了解泛型的原理、常见的使用方法以及泛型的局限性,让你对泛型有更深入的了解。
33 2
Java 泛型详细解析
|
18天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
20天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
20天前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
22天前
|
Java 数据库连接 开发者
Java中的异常处理机制:深入解析与最佳实践####
本文旨在为Java开发者提供一份关于异常处理机制的全面指南,从基础概念到高级技巧,涵盖try-catch结构、自定义异常、异常链分析以及最佳实践策略。不同于传统的摘要概述,本文将以一个实际项目案例为线索,逐步揭示如何高效地管理运行时错误,提升代码的健壮性和可维护性。通过对比常见误区与优化方案,读者将获得编写更加健壮Java应用程序的实用知识。 --- ####
|
3月前
|
Java 索引
java基础(13)String类
本文介绍了Java中String类的多种操作方法,包括字符串拼接、获取长度、去除空格、替换、截取、分割、比较和查找字符等。
45 0
java基础(13)String类
|
25天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
41 2
|
2月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
68 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
|
2月前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
52 2
|
3月前
|
安全 Java
String类-知识回顾①
这篇文章回顾了Java中String类的相关知识点,包括`==`操作符和`equals()`方法的区别、String类对象的不可变性及其好处、String常量池的概念,以及String对象的加法操作。文章通过代码示例详细解释了这些概念,并探讨了使用String常量池时的一些行为。
String类-知识回顾①

推荐镜像

更多