赞
踩
String虽然是线程安全的,但是在某些场合的表现不尽如人意,上篇文章也分析了,String底层的数据存储借助了final修饰的字符数组,一旦创建便不能修改。
“宏观”上的字符串截取、拼接等操作,都是通过创建新的对象实现的,涉及大量的数组拷贝,性能不佳。
StringBuffer和StringBuilder底层也是字符数组,但是并没有用final修饰,是可变的。
在考虑线程安全的同时,我们也要去了解其内部的存储机制、扩容机制,以及StringBuffer中的二级缓存。
StringBuilder与StringBuffer处于同一层级,就不重复贴图了。
CharSequence和Serializable都是老朋友了,我们看下Appendable接口
Appendable主要是为字符序列(有序字符的集合)拼接规范接口。
一个抽象类居然独占一个小标题?
因为这个抽象类是一个重量级的抽象类,只有一个抽象方法,其他所有方法都有对应的实现。
- @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)方法为例
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不仅继承了父类的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是真的“安全”!
才疏学浅,如有错误,欢迎批评指正!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。