【jdk源码阅读】第三篇:StringBuffer、StringBuilder源码分析

String虽然是线程安全的,但是在某些场合的表现不尽如人意,上篇文章也分析了,String底层的数据存储借助了final修饰的字符数组,一旦创建便不能修改。

“宏观”上的字符串截取、拼接等操作,都是通过创建新的对象实现的,涉及大量的数组拷贝,性能不佳。

StringBuffer和StringBuilder底层也是字符数组,但是并没有用final修饰,是可变的。

在考虑线程安全的同时,我们也要去了解其内部的存储机制、扩容机制,以及StringBuffer中的二级缓存。

继承体系

StringBuilder与StringBuffer处于同一层级,就不重复贴图了。

CharSequence和Serializable都是老朋友了,我们看下Appendable接口

Appendable主要是为字符序列(有序字符的集合)拼接规范接口。

AbstractStringBuilder

一个抽象类居然独占一个小标题?

因为这个抽象类是一个重量级的抽象类,只有一个抽象方法,其他所有方法都有对应的实现。

    @Override
    public abstract String toString();

就算不看源码,我们也应该知道StringBuffer和StringBuilder主要的区别在于是否线程安全。

在实现的功能上、包括对外开放的接口都是一样的,只是StringBuffer可能会对某些共享资源或者某些方法做一些访问控制。

除此之外,StringBuffer为了提高重复调用toString方法时得性能,提供了一个“二级缓存”,toString()方法与StringBuilder的实现不太一致,否则我觉得这俩兄弟甚至都可以共同继承一个实现全功能的类了(而不仅仅是抽象类)。

 

下面先分析一下AbstractStringBuilder这个类,包含一些字符串拼装逻辑、以及关键的buffer扩容机制等。

成员变量

    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;

value用于存储字符序列,count用于记录字符的长度,这些变量主要是为子类服务的,为了保证对其子类的可见性,没有定义为private。

扩容机制

虽然定义了字符数组,但并没有申请空间,为了保证Buffer的正常使用,必然会在构造函数执行期间执行初始化操作。

    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

如果容量不够,则需要扩容。
先看下面的两个函数

    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));
        }
    }

传入一个最保守的期望值,如果当前数组长度不满足需求,则会扩容。

扩容时,会将已有的数据拷贝到一个新的数组,新数组的长度由newCapacity方法决定。

    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

    private int hugeCapacity(int minCapacity) {
        if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
            throw new OutOfMemoryError();
        }
        return (minCapacity > MAX_ARRAY_SIZE)
            ? minCapacity : MAX_ARRAY_SIZE;
    }

int newCapacity = (value.length << 1) + 2;    左移可以使原来的数扩大2的n次方倍

新数组长度 =  现有数组的长度 *  2   +  2

方法的最后,对于边界值做了一点处理。

不知道有没有人有同样的疑问,为什么会加 newCapacity < 0这个条件?

因为移位运算是有溢出风险的。(代码中也标出来了, overflow-conscious code)

举个栗子

public class StringBuilderMain {
    public static void main(String[] args) {
        int newCapacity = ((0x7fffffff - 100) << 1) + 2;
        System.out.println(newCapacity);
    }
}

我们取比Integer最大值小200的数,得到的结果

确实是负数。

注:

0x7fffffff = Integer.MAX_VALUE

 

AbstractStringBuilder中定义的数组最大值为

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

如果扩容时,想要指定一个更大的值,会直接抛出OOM异常。

扩容时机

当需要向数组里填充数据时,就需要检查数组的剩余的空间是否满足需求了。

以append(String)方法为例

StringBuilder

基本实现

jdk1.5以后才有的StringBuilder 。

在父类的基础上,用上了建造者模式,大部分方法形如:

在父类的基础上,并没有做什么加工。

构造期间,给字符数组指定的初始长度为16

重写了toString方法

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

点进String的构造函数,应该可以发现,本质也是数组拷贝(注意这里与StringBuffer的区别)

多线程环境

StringBuilder中的共享资源继承自父类 

value数组与统计字符数量的count变量都是可更改的,没有做访问控制(是不安全的)。

StringBuffer

StringBuffer不仅继承了父类的char数组,还定义了额外的数组做缓存。(transient表示不参与序列化,毕竟这只是个缓存,仅在本地使用)

    /**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;

当数组的值发生更改时,会清除缓存

同时我们可以发现,与共享变量相关的方法,都加了锁,在多线程环境下可以放心使用。

缓存数组一直在被清空,直到调用toString()方法,才会用上缓存。

    @Override
    public synchronized String toString() {
        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);
        }
        return new String(toStringCache, true);
    }

我们刚才看StringBuilder的源码,发现toString()方法返回字符串时,依然进行了数组拷贝。

而StringBuffer这边做了优化,如果重复调用toString方法,并且中间没有修改过Buffer,那么只需要进行一次数组拷贝即可。

我们进入String的构造函数

    String(char[] value, boolean share) {
        // assert share : "unshared not supported";
        this.value = value;
    }

发现它给StringBuffer开了捷径,直接引用复制,没有数组拷贝。

其实这么做是相当危险的,为了说明这个问题,我自定义一个String

public class MyString {
    private final char[]arr;

    public MyString(char[]arr) {
        this.arr = arr;
    }

    public void print() {
        for (char c : arr) {
            System.out.print(c + "\t");
        }
        System.out.println("\n");
    }

    public static void main(String[] args) {
        char[] chars = new char[]{'a', 'b'};
        MyString myString = new MyString(chars);
        myString.print();
        chars[0] = 'c';
        myString.print();
    }
}

输出结果

final修饰的数组,如果通过引用赋值来初始化自己,是不安全的,因为可以通过别的引用(代码中chars数组)可以修改数组的值。

AbstractStringBuilder维护的数组是一直在变化的,不可直接给String中的char数组引用赋值,所以StringBuilder中的toString()方法才会不厌其烦地进行数组拷贝。

StringBuffer为了线程安全,方法加了锁,性能上已经做了牺牲,所以jdk给他又准备了一个缓存。

private transient char[] toStringCache;

与这个缓存的代码,其实只有两种形式

第一种:置为null

toStringCache = null;

第二种:数组拷贝式赋值

    @Override
    public synchronized String toString() {
        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);
        }
        return new String(toStringCache, true);
    }

不会通过toStringCache这个引用去修改数组的值,所以这个二级缓存是安全的,可以放心的交给String。

String也很给面子,敲敲开了后门。大伙应该注意到了吧,String的这个构造函数没有用public修饰,所以只能同包的类使用(java.lang),我们在开发过程中是不能直接用这个构造函数的。

所以说,Java是真的“安全”!

 

才疏学浅,如有错误,欢迎批评指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值