Java 中String 类的不可变性与字符串拼接解析

@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" ;

字符串拼接的全过程如下:

  1. 临时创建一个StringBuffer 对象,并调用其append(String str) 方法进行字符串的连接操作
  2. 调用StringBuffer 对象的toString() 方法转换成String 对象,其内容为“ab"
  3. 将生成的String 对象赋值给变量string

这里要注意一点:

当使用运算符"+" 连接字符串时,如果两个操作数都是编译时常量,则在编译期就会计算出该字符串的值,而不会在运行时创建StringBuffer 或 StringBuilder 对象。

在实际编码时,我们常提倡不使用"+" 反复进行String 对象的连接,而是直接使用StringBuffer 或 StringBuilder 来连接的原因就是可以省去每次系统创建StringBuffer 或 StringBuilder的开销。

至此,本文结束。我是陈冰安,一个Java学习者。欢迎关注我的公众号【暗星涌动】,愿与你一同进步。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345