本博文主要介绍JDK源码中的String、StringBuffer、StringBuilder、StringJoiner、StringTokenizer源码分析。帮助大家更好的学习和了解JDK源码与解答面试问题。
一、String源码分析 1.1 String不可修改特性对于初学者来说,很容易误认为String对象是可以改变的,特别是+链接时,对象似乎真的改变了。然而String对象一经创建就不可以修改。原因有一下几点:
- String底层指向的是一个Character类型的数组,在常量池中数组保持不可修改,只能修改其应用的作用主要是为了实现了常量池的变量复用,同时减少重复常量。
- 如果string是可变的,那么在底层计算一个String的hashcode也将改变。可能导致发生Hash碰撞的问题。
public final class String
implements java.io.Serializable, Comparable, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
}
我们可以发现 String是一个final类,且3个成员都是私有的,这就意味着String是不能被继承的,这就防止出现:程序员通过继承重写String类的方法的手段来使得String类是“可变的”的情况。从源码发现,每个String对象维护着一个char数组私有成员value。数组value 是String的底层数组,用于存储字符串的内容,而且是 private final ,但是数组是引用类型,所以只能限制引用不改变而已,也就是说数组元素的值是可以改变的。
public static void main(String[] args) {
char[] arr = new char[]{'a','b','c','d'};
String str = new String(arr);
arr[3]='e';
System.out.println("str= "+str);
System.out.println("arr[]= "+Arrays.toString(arr));
}
---------------------------------------------------------
运行结果
1 str= abcd
2 arr[]= [a, b, c, e]
结果与我们所想不一样。字符串str使用数组arr来构造一个对象,当数组arr修改其元素值后,字符串str并没有跟着改变。那就看一下这个构造方法是怎么处理的:
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
原来String在使用外部char数组构造对象时,是重新复制了一份外部char数组,从而不会让外部char数组的改变影响到String对象。
从上面的分析我们知道,我们是无法从外部修改String对象的,那么可不可能使用String提供的方法,因为有不少方法看起来是可以改变String对象的,如replace()
、replaceAll()
、substring()
等。我们以substring()
为例,
public String substring(int beginIndex, int endIndex) {
//........
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
从源码可以看出,如果不是切割整个字符串的话,就会新建一个对象。也就是说,只要与原字符串不相等,就会新建一个String对象。
1.2 缓存HashcodeJava中经常会用到字符串的哈希码(hashcode)。例如,在HashMap中,字符串的不可变能保证其hashcode永远保持一致,这样就可以避免一些不必要的麻烦。这也就意味着每次在使用一个字符串的hashcode的时候不用重新计算一次,这样更加高效。在String类中,有以下代码:
private int hash;//this is used to cache hash code.
以上代码中hash
变量中就保存了一个String对象的hashcode,因为String类不可变,所以一旦对象被创建,该hash值也无法改变。所以,每次想要使用该对象的hashcode的时候,直接返回即可。
其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。下面一段字符串拼接代码:
1 String s = "abcd";
2 s = s.concat("ef");
其实最后我们得到的s已经是一个新的字符串了。s中保存的一个重新创建出来的String对象的引用.
通过new关键字创建字符串对象一定在堆中。构造器中传入了以双引号引起的字符串对象,则会去字符串常量池中找有没有这个字符串对象,有的话直接指向,没有的话就创建。,通过字面量方式(区别于new)给一个String类型引用变量赋值,此时的字符串直接存储在方法区的字符串常量池中(编译期就可以确定)。并且凡是双引号括起来的都在字符串常量池中仅有一份,不会产生多个副本。String类是一个不可变类,一旦一个String对象创建后,包含在这个对象中的字符序列是不可修改的,直到这个对象被销毁。通俗讲就是通过双引号括起来的字符串是不可变的,从出生到死亡都不可变。
凡是双引号括起来的都在字符串常量池中从创建到销毁是不可变的且只有一份。代码中虽然有3个双引号括起来的字符串,但“java”是重复的因此只有一份。只要双引号引起来就是一个String对象,所以拼接中的“EE”也会在字符串常量池中存在。创建s2时,字符串常量池中没有对应的字符串,所以会去创建新的。此时会发现“EE”是个额外产生的占用字符串常量池空间的String对象。
String类底层用一个char数组来存储数据,数组的长度一旦确定是不可变的并且该char数组的引用类型变量value是由final修饰符修饰的,所以value引用的数组地址也是不能改变的。
StringBuffer
和StringBuilder
的实现原理和String
类类似。
这两个类的实现原理基本相同,但StringBuffer是线程安全的,StringBuilder是非线程安全的。所以如果分析这两个类避不开AbstractStringBuilder抽象类。因为这个抽象类是一个重量级的抽象类,只有一个抽象方法,其他所有方法都有对应的实现。
@Override
public abstract String toString();
StringBuffer和StringBuilder他们两都继承了AbstractStringBuilder抽象类,他们的底层都是可变的字符数组,所以在进行频繁的字符串操作时,建议使用StringBuffer和 StringBuilder来进行操作。 另外StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
在多线程中推荐使用StringBuffer,如果在单线程情况中使用StringBuilder会更好,StringBuilder没有锁,执行速度会更快。StringBuffer和StringBuilder都比String执行速度块。
String是不可变的字符串缓存区,而StringBuffer和StringBuffer是可变的字符串缓存区。
AbstractStringBuilder这个类成员变量
char[] value;
int count;
value用于存储字符序列,count用于记录字符的长度,这些变量主要是为子类服务的,为了保证对其子类的可见性,没有定义为private。
2.1 AbstractStringBuilder扩容机制虽然定义了字符数组,但并没有申请空间,为了保证Buffer的正常使用,必然会在构造函数执行期间执行初始化操作。AbstractStringBuilder中定义的数组最大值为如下,如果扩容时,想要指定一个更大的值,会直接抛出OOM异常。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
如果容量不够,则需要扩容。传入一个最保守的期望值,如果当前数组长度不满足需求,则会扩容。扩容时,会将已有的数据拷贝到一个新的数组,新数组的长度由newCapacity方法决定。
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));
}
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?