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

一、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学习者。欢迎关注我的公众号【暗星涌动】,愿与你一同进步。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暗星涌动

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值