Java StringBuffer StringBuilder类源码解析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 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);
    }
相关文章
|
16天前
|
数据采集 人工智能 Java
Java产科专科电子病历系统源码
产科专科电子病历系统,全结构化设计,实现产科专科电子病历与院内HIS、LIS、PACS信息系统、区域妇幼信息平台的三级互联互通,系统由门诊系统、住院系统、数据统计模块三部分组成,它管理了孕妇从怀孕开始到生产结束42天一系列医院保健服务信息。
28 4
|
23天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
60 2
|
10天前
|
Java 数据库连接 开发者
Java中的异常处理机制:深入解析与最佳实践####
本文旨在为Java开发者提供一份关于异常处理机制的全面指南,从基础概念到高级技巧,涵盖try-catch结构、自定义异常、异常链分析以及最佳实践策略。不同于传统的摘要概述,本文将以一个实际项目案例为线索,逐步揭示如何高效地管理运行时错误,提升代码的健壮性和可维护性。通过对比常见误区与优化方案,读者将获得编写更加健壮Java应用程序的实用知识。 --- ####
|
19天前
|
Java 测试技术 API
Java 反射机制:深入解析与应用实践
《Java反射机制:深入解析与应用实践》全面解析Java反射API,探讨其内部运作原理、应用场景及最佳实践,帮助开发者掌握利用反射增强程序灵活性与可扩展性的技巧。
50 4
|
24天前
|
存储 算法 Java
Java Set深度解析:为何它能成为“无重复”的代名词?
Java的集合框架中,Set接口以其“无重复”特性著称。本文解析了Set的实现原理,包括HashSet和TreeSet的不同数据结构和算法,以及如何通过示例代码实现最佳实践。选择合适的Set实现类和正确实现自定义对象的hashCode()和equals()方法是关键。
26 4
|
23天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
20天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
11天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
6天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####

推荐镜像

更多