Java StringBuffer StringBuilder类源码解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 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);
    }
相关文章
|
4天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
16 2
|
15天前
|
存储 安全 Java
java.util的Collections类
Collections 类位于 java.util 包下,提供了许多有用的对象和方法,来简化java中集合的创建、处理和多线程管理。掌握此类将非常有助于提升开发效率和维护代码的简洁性,同时对于程序的稳定性和安全性有大有帮助。
38 17
|
4天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
7天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
11天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
51 4
|
12天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
27 2
|
9天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
18天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
5天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9
|
8天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####

推荐镜像

更多