@TOC
一、String 类是不可变的
1.1 不可变的原因
在Java 中,对于String 类的定义如下:
由图可知,String 类的值存储于其私有变量value 中,而变量 value 是final 修饰的。
而在Java 中,final 修饰引用变量时代表给该引用无法修改其对象但可以改变其状态。
同时经过阅读源码,我们发现,在String 类中 value 是私有变量且没有提供对应的setter 方法;除了构造方法外,类中的方法也不会触碰value 中的元素。
以substring(int beginIndex,int endIndex) 方法为例,该方法并不会去修改当前String 对象的任何变量,而是使用 new String(...) 直接创建一个新的String 对象或返回自身。
也就是说,对于String 对象,一旦我们对其进行了赋值,该对象中的value 变量也就不再会有变化了。
这就是为什么我们说String 类是不可变类的原因。
1.2 不可变的好处
String 类是不可变类,其对象已经创建就不能进行修改。因此,在多线程操作时,可以认为其是不变的,不用担心其他线程有意或无意间对其进行了修改。
String 类得不可变性使其性质类似于只读得共享文件,不用担心因并发操作而导致的一系列问题,也就没必要使用线程同步操作,简单方便。
二、字符串的"+" 拼接
2.1 官方解释
在oracle jdk 8 的官方文档中关于Sting 类的描述中有这么一段文字:
The Java language provides special support for the string concatenation operator ( + ), and for conversion of other objects to strings. String concatenation is implemented through the StringBuilder(or StringBuffer) class and its append method.
具体的翻译是:
Java 语言提供对字符串串联符号( " + " )以及将其他对象转换为字符串的特殊支持。字符串串联是通过 StringBuilder(或StringBuffer )类及其 append 方法实现的。
2.2 append() 方法
通过查看StringBuffer 类的源码,可以得知:对于append(...) 方法,在StringBuffer 类中并没有进行覆写,而是直接调用其父类的方法来实现。
而StringBuffer 类的父类是AbstractStringBuilder 抽象类。
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
2.3 具体实现
在AbstractStringBuilder 抽象类中对于append(...) 方法的定义有很多,我们主要看参数为String 类型的方法,其他的均是类似的过程:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
private AbstractStringBuilder appendNull() {
int c = count;
ensureCapacityInternal(c + 4);
final char[] value = this.value;
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
count = c;
return this;
}
String 中对于getChars(...) 方法的定义:
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
对于以上两段源码,其中,
- ensureCapacityInternal(...) 方法的主要作用是扩充value 数组的大小,其实现最后还是使用System.arraycopy(...) 方法实现对原数组的复制。
- System.arraycopy(...) 方法的作用就是实现数组之间的复制。
2.4 源码解析
2.4.1 方法解析
对于System.arraycopy(...) 方法:
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
参数解释:
- Object src : 源数组
- int srcPos : 源数组的起始位置
- Object dest : 目标数组
- int destPos : 目标数组的起始位置
- int length : 复制长度
实现功能:
将参数str 从下标为srcPos 的位置开始,总共length 个字符( 在实际中不一定是str 的总长度 ) 复制到变量dest 中从下标为destPos 开始的位置。
2.4.2 实际调用
在append(String str) 方法调用过程中,对于最后的System.arraycopy(...) 方法,其参数具体如下:
System.arraycopy(String.value, 0, AbstractStringBuilder.value, AbstractStringBuilder.count, str.length());
其中,
- String.value实际上就是append(String str) 方法的参数str ;
- 0 表示从str 下标为0 的位置开始复制;
- AbstractStringBuilder.value 实际上就是StringBuffer 的字符数组,也就是说str 字符串最后添加到了StringBuffer 的字符数组后面;
- AbstractStringBuilder.count 是AbstractStringBuilder 类中字符数组value 的长度( 即StringBuffer 的字符数组的长度 ),表示第一个复制字符的存储位置是value[count] ;
- str.length() 顾名思义,指参数str 的长度,也就是说,此次复制是复制整个str 字符串。
也就是说,在实际的运行过程中,调用的System.arraycopy(...) 方法是这样子的:
System.arraycopy(str, 0, value, count, len);
因为count= value.length 并且 len=str.length,所以该方法实现的功能是:
将字符串str 拼接到字符数组value 之后,这里的value 指的是AbstractStringBuilder 类中的字符数组value ,也是StringBuffer 的字符数组。
2.5 转换成String
经过前面的几步,字符串str 已经成功拼接到了StringBuffer 的字符数组之后了,但是StringBuffer 又是怎么转换成String 类的呢?
其实很简单,只需要调用StringBuffer 的toString() 方法即可。
StringBuffer 类中对于toString() 方法的定义如下:
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
String 类的构造方法:
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
由上述源码可以发现,当调用StringBuffer 类的toString() 方法时,会自动新建一个String 对象,用以存储StringBuffer 类中字符数组的值。
三、总结
3.1 String 类的不可变性
String 类是不可变类的原因:
- String 类的值封装在字符数组value 中,而value 是被final 修饰的,不可以改变对象
- 字符数组value 是私有变量,且没有提供setter 方法
- 除了构造方法,在String 类中的方法里都没有触碰字符数组value 里的元素
3.2 使用"+" 进行字符串拼接的过程
对于代码
String str = new String("a") ;
String string = str + "b" ;
字符串拼接的全过程如下:
- 临时创建一个StringBuffer 对象,并调用其append(String str) 方法进行字符串的连接操作
- 调用StringBuffer 对象的toString() 方法转换成String 对象,其内容为“ab"
- 将生成的String 对象赋值给变量string
这里要注意一点:
当使用运算符"+" 连接字符串时,如果两个操作数都是编译时常量,则在编译期就会计算出该字符串的值,而不会在运行时创建StringBuffer 或 StringBuilder 对象。
在实际编码时,我们常提倡不使用"+" 反复进行String 对象的连接,而是直接使用StringBuffer 或 StringBuilder 来连接的原因就是可以省去每次系统创建StringBuffer 或 StringBuilder的开销。
至此,本文结束。我是陈冰安,一个Java学习者。欢迎关注我的公众号【暗星涌动】,愿与你一同进步。